1 /** 2 * @file opannotate.cpp 3 * Implement opannotate utility 4 * 5 * @remark Copyright 2003 OProfile authors 6 * @remark Read the file COPYING 7 * 8 * @author John Levon 9 * @author Philippe Elie 10 */ 11 12 #include <iostream> 13 #include <sstream> 14 #include <algorithm> 15 #include <iomanip> 16 #include <fstream> 17 #include <utility> 18 19 #include "op_exception.h" 20 #include "op_header.h" 21 #include "profile.h" 22 #include "populate.h" 23 #include "op_sample_file.h" 24 #include "cverb.h" 25 #include "string_manip.h" 26 #include "demangle_symbol.h" 27 #include "child_reader.h" 28 #include "op_file.h" 29 #include "file_manip.h" 30 #include "arrange_profiles.h" 31 #include "opannotate_options.h" 32 #include "profile_container.h" 33 #include "symbol_sort.h" 34 #include "image_errors.h" 35 36 using namespace std; 37 using namespace options; 38 39 namespace { 40 41 size_t nr_events; 42 43 scoped_ptr<profile_container> samples; 44 45 /// how opannotate was invoked 46 string cmdline; 47 48 /// empty annotation fill string 49 string annotation_fill; 50 51 /// string used as start / end comment to annotate source 52 string const begin_comment("/* "); 53 string const in_comment(" * "); 54 string const end_comment(" */"); 55 56 /// field width for the sample count 57 unsigned int const count_width = 6; 58 59 string get_annotation_fill() 60 { 61 string str; 62 63 for (size_t i = 0; i < nr_events; ++i) { 64 str += string(count_width, ' ') + ' '; 65 str += string(percent_width, ' '); 66 } 67 68 for (size_t i = 1; i < nr_events; ++i) 69 str += " "; 70 71 str += " :"; 72 return str; 73 } 74 75 76 symbol_entry const * find_symbol(string const & image_name, 77 string const & str_vma) 78 { 79 // do not use the bfd equivalent: 80 // - it does not skip space at begin 81 // - we does not need cross architecture compile so the native 82 // strtoull must work, assuming unsigned long long can contain a vma 83 // and on 32/64 bits box bfd_vma is 64 bits 84 bfd_vma vma = strtoull(str_vma.c_str(), NULL, 16); 85 86 return samples->find_symbol(image_name, vma); 87 } 88 89 90 void output_info(ostream & out) 91 { 92 out << begin_comment << '\n'; 93 94 out << in_comment << "Command line: " << cmdline << '\n' 95 << in_comment << '\n'; 96 97 out << in_comment << "Interpretation of command line:" << '\n'; 98 99 if (!assembly) { 100 out << in_comment 101 << "Output annotated source file with samples" << '\n'; 102 103 if (options::threshold != 0) { 104 out << in_comment 105 << "Output files where samples count reach " 106 << options::threshold << "% of the samples\n"; 107 } else { 108 out << in_comment << "Output all files" << '\n'; 109 } 110 } else { 111 out << in_comment 112 << "Output annotated assembly listing with samples" 113 << '\n'; 114 115 if (!objdump_params.empty()) { 116 out << in_comment << "Passing the following " 117 "additional arguments to objdump ; \""; 118 for (size_t i = 0 ; i < objdump_params.size() ; ++i) 119 out << objdump_params[i] << " "; 120 out << "\"" << '\n'; 121 } 122 } 123 124 out << in_comment << '\n'; 125 126 out << in_comment << classes.cpuinfo << endl; 127 if (!classes.event.empty()) 128 out << in_comment << classes.event << endl; 129 130 for (size_t i = 0; i < classes.v.size(); ++i) 131 out << in_comment << classes.v[i].longname << endl; 132 133 out << end_comment << '\n'; 134 } 135 136 137 string count_str(count_array_t const & count, 138 count_array_t const & total) 139 { 140 ostringstream os; 141 for (size_t i = 0; i < nr_events; ++i) { 142 os << setw(count_width) << count[i] << ' '; 143 144 os << format_percent(op_ratio(count[i], total[i]) * 100.0, 145 percent_int_width, percent_fract_width); 146 } 147 return os.str(); 148 } 149 150 151 /// NOTE: This function annotates a list<string> containing output from objdump. 152 /// It uses a list iterator, and a sample_container iterator which iterates 153 /// from the beginning to the end, and compare sample address 154 /// against the instruction address on the asm line. 155 /// 156 /// There are 2 cases of annotation: 157 /// 1. If sample address matches current line address, annotate the current line. 158 /// 2. If (previous line address < sample address < current line address), 159 /// then we annotate previous line. This case happens when sample address 160 /// is not aligned with the instruction address, which is seen when profile 161 /// using the instruction fetch mode of AMD Instruction-Based Sampling (IBS). 162 /// 163 int asm_list_annotation(symbol_entry const * last_symbol, 164 bfd_vma last_symbol_vma, 165 list<string>::iterator sit, 166 sample_container::samples_iterator & samp_it, 167 list<string> & asm_lines) 168 { 169 int ret = 0; 170 171 sample_entry const * sample = NULL; 172 173 if (samp_it != samples->end()) 174 sample = &samp_it->second; 175 176 // do not use the bfd equivalent: 177 // - it does not skip space at begin 178 // - we does not need cross architecture compile so the native 179 // strtoull must work, assuming unsigned long long can contain a vma 180 // and on 32/64 bits box bfd_vma is 64 bits 181 // gcc 2.91.66 workaround 182 bfd_vma vma = strtoull((*sit).c_str(), NULL, 16); 183 184 if (sample 185 && ((sample->vma < last_symbol_vma) || (sample->vma > vma))) { 186 *sit = annotation_fill + *sit; 187 } else if (sample && sample->vma == vma) { 188 // Case 1 : Sample address match current line address. 189 string str = count_str(sample->counts, samples->samples_count()); 190 191 // For each events 192 for (size_t i = 1; i < nr_events; ++i) 193 str += " "; 194 195 *sit = str + " :" + *sit; 196 if (samp_it != samples->end()) 197 ++samp_it; 198 199 } else if (sample && sample->vma < vma) { 200 // Case 2 : vma of the current line is greater than vma of the sample 201 202 // Get the string of previous assembly line 203 list<string>::iterator sit_prev = sit; 204 string prev_line, prev_vma_str; 205 string::size_type loc1 = string::npos, loc2 = string::npos; 206 while (sit_prev != asm_lines.begin()) { 207 --sit_prev; 208 prev_line = *sit_prev; 209 210 loc1 = prev_line.find(":", 0); 211 if (loc1 != string::npos) { 212 loc2 = prev_line.find(":", loc1+1); 213 if (loc2 != string::npos) { 214 prev_vma_str = prev_line.substr(loc1+1, loc2); 215 break; 216 } 217 } 218 } 219 220 bfd_vma prev_vma = strtoull(prev_vma_str.c_str(), NULL, 16); 221 222 // Need to check if prev_vma < sample->vma 223 if (prev_vma != 0 && prev_vma < sample->vma) { 224 string str; 225 226 // Get sample for previous line. 227 sample_entry * prev_sample = (sample_entry *)samples-> 228 find_sample(last_symbol, prev_vma); 229 if (prev_sample) { 230 // Aggregate sample with previous line if it already has samples 231 prev_sample->counts += sample->counts; 232 str = count_str(prev_sample->counts, samples->samples_count()); 233 } else { 234 str = count_str(sample->counts, samples->samples_count()); 235 } 236 237 // For each events 238 for (size_t i = 1; i < nr_events; ++i) 239 str += " "; 240 241 *sit_prev = str + " :" + prev_line.substr(loc1+1); 242 if (samp_it != samples->end()) 243 ++samp_it; 244 ret = -1; 245 } else { 246 // Failed to annotate the previous line. Skip sample. 247 *sit = annotation_fill + *sit; 248 if (samp_it != samples->end()) 249 ++samp_it; 250 } 251 } else { 252 // In case sample is NULL 253 *sit = annotation_fill + *sit; 254 } 255 256 return ret; 257 } 258 259 260 string symbol_annotation(symbol_entry const * symbol) 261 { 262 if (!symbol) 263 return string(); 264 265 string annot = count_str(symbol->sample.counts, 266 samples->samples_count()); 267 268 string const & symname = symbol_names.demangle(symbol->name); 269 270 string str = " "; 271 str += begin_comment + symname + " total: "; 272 str += count_str(symbol->sample.counts, samples->samples_count()); 273 str += end_comment; 274 return str; 275 } 276 277 278 /// return true if this line contains a symbol name in objdump formatting 279 /// symbol are on the form 08030434 <symbol_name>: we need to be strict 280 /// here to avoid any interpretation of a source line as a symbol line 281 bool is_symbol_line(string const & str, string::size_type pos) 282 { 283 if (str[pos] != ' ' || str[pos + 1] != '<') 284 return false; 285 286 return str[str.length() - 1] == ':'; 287 } 288 289 290 void annotate_objdump_str_list(string const & app_name, 291 symbol_collection const & symbols, 292 list<string> & asm_lines) 293 { 294 symbol_entry const * last_symbol = 0; 295 bfd_vma last_symbol_vma = 0; 296 int ret = 0; 297 298 // to filter output of symbols (filter based on command line options) 299 bool do_output = true; 300 301 // We simultaneously walk the two structures (list and sample_container) 302 // which are sorted by address. and do address comparision. 303 list<string>::iterator sit = asm_lines.begin(); 304 list<string>::iterator send = asm_lines.end(); 305 sample_container::samples_iterator samp_it = samples->begin(); 306 307 for (; sit != send; (!ret? sit++: sit)) { 308 // output of objdump is a human readable form and can contain some 309 // ambiguity so this code is dirty. It is also optimized a little bit 310 // so it is difficult to simplify it without breaking something ... 311 312 // line of interest are: "[:space:]*[:xdigit:]?[ :]", the last char of 313 // this regexp dis-ambiguate between a symbol line and an asm line. If 314 // source contain line of this form an ambiguity occur and we rely on 315 // the robustness of this code. 316 string str = *sit; 317 size_t pos = 0; 318 while (pos < str.length() && isspace(str[pos])) 319 ++pos; 320 321 if (pos == str.length() || !isxdigit(str[pos])) { 322 if (do_output) { 323 *sit = annotation_fill + str; 324 continue; 325 } 326 } 327 328 while (pos < str.length() && isxdigit(str[pos])) 329 ++pos; 330 331 if (pos == str.length() || (!isspace(str[pos]) && str[pos] != ':')) { 332 if (do_output) { 333 *sit = annotation_fill + str; 334 continue; 335 } 336 } 337 338 if (is_symbol_line(str, pos)) { 339 340 last_symbol = find_symbol(app_name, str); 341 last_symbol_vma = strtoull(str.c_str(), NULL, 16); 342 343 // ! complexity: linear in number of symbol must use sorted 344 // by address vector and lower_bound ? 345 // Note this use a pointer comparison. It work because symbols 346 // pointer are unique 347 if (find(symbols.begin(), symbols.end(), last_symbol) 348 != symbols.end()) 349 do_output = true; 350 else 351 do_output = false; 352 353 if (do_output) { 354 *sit += symbol_annotation(last_symbol); 355 356 // Realign the sample iterator to 357 // the beginning of this symbols 358 samp_it = samples->begin(last_symbol); 359 } 360 } else { 361 // not a symbol, probably an asm line. 362 if (do_output) 363 ret = asm_list_annotation(last_symbol, 364 last_symbol_vma, 365 sit, samp_it, 366 asm_lines); 367 } 368 369 if (!do_output) 370 *sit = ""; 371 } 372 } 373 374 375 void output_objdump_str_list(symbol_collection const & symbols, 376 string const & app_name, 377 list<string> & asm_lines) 378 { 379 380 annotate_objdump_str_list(app_name, symbols, asm_lines); 381 382 // Printing objdump output to stdout 383 list<string>::iterator sit = asm_lines.begin(); 384 list<string>::iterator send = asm_lines.end(); 385 sit = asm_lines.begin(); 386 for (; sit != send; ++sit) { 387 string str = *sit; 388 if (str.length() != 0) 389 cout << str << '\n'; 390 } 391 } 392 393 394 void do_one_output_objdump(symbol_collection const & symbols, 395 string const & image_name, string const & app_name, 396 bfd_vma start, bfd_vma end) 397 { 398 vector<string> args; 399 list<string> asm_lines; 400 401 args.push_back("-d"); 402 args.push_back("--no-show-raw-insn"); 403 if (source) 404 args.push_back("-S"); 405 406 if (start || end != ~(bfd_vma)0) { 407 ostringstream arg1, arg2; 408 arg1 << "--start-address=" << start; 409 arg2 << "--stop-address=" << end; 410 args.push_back(arg1.str()); 411 args.push_back(arg2.str()); 412 } 413 414 if (!objdump_params.empty()) { 415 for (size_t i = 0 ; i < objdump_params.size() ; ++i) 416 args.push_back(objdump_params[i]); 417 } 418 419 args.push_back(image_name); 420 #if defined(ANDROID) 421 child_reader reader("arm-eabi-objdump", args); 422 #else 423 child_reader reader("objdump", args); 424 #endif 425 if (reader.error()) { 426 cerr << "An error occur during the execution of objdump:\n\n"; 427 cerr << reader.error_str() << endl; 428 return; 429 } 430 431 // Read each output line from objdump and store in a list. 432 string str; 433 while (reader.getline(str)) 434 asm_lines.push_back(str); 435 436 output_objdump_str_list(symbols, app_name, asm_lines); 437 438 // objdump always returns SUCCESS so we must rely on the stderr state 439 // of objdump. If objdump error message is cryptic our own error 440 // message will be probably also cryptic 441 ostringstream std_err; 442 ostringstream std_out; 443 reader.get_data(std_out, std_err); 444 if (std_err.str().length()) { 445 cerr << "An error occur during the execution of objdump:\n\n"; 446 cerr << std_err.str() << endl; 447 return ; 448 } 449 450 // force error code to be acquired 451 reader.terminate_process(); 452 453 // required because if objdump stop by signal all above things suceeed 454 // (signal error message are not output through stdout/stderr) 455 if (reader.error()) { 456 cerr << "An error occur during the execution of objdump:\n\n"; 457 cerr << reader.error_str() << endl; 458 return; 459 } 460 } 461 462 463 void output_objdump_asm(symbol_collection const & symbols, 464 string const & app_name) 465 { 466 image_error error; 467 string image = 468 classes.extra_found_images.find_image_path(app_name, error, 469 true); 470 471 // this is only an optimisation, we can either filter output by 472 // directly calling objdump and rely on the symbol filtering or 473 // we can call objdump with the right parameter to just disassemble 474 // the needed part. This is a real win only when calling objdump 475 // a medium number of times, I dunno if the used threshold is optimal 476 // but it is a conservative value. 477 size_t const max_objdump_exec = 50; 478 if (symbols.size() <= max_objdump_exec || error != image_ok) { 479 symbol_collection::const_iterator cit = symbols.begin(); 480 symbol_collection::const_iterator end = symbols.end(); 481 for (; cit != end; ++cit) { 482 bfd_vma start = (*cit)->sample.vma; 483 bfd_vma end = start + (*cit)->size; 484 do_one_output_objdump(symbols, image, app_name, 485 start, end); 486 } 487 } else { 488 do_one_output_objdump(symbols, image, 489 app_name, 0, ~bfd_vma(0)); 490 } 491 } 492 493 494 bool output_asm(string const & app_name) 495 { 496 profile_container::symbol_choice choice; 497 choice.threshold = options::threshold; 498 choice.image_name = app_name; 499 choice.match_image = true; 500 symbol_collection symbols = samples->select_symbols(choice); 501 502 if (!symbols.empty()) { 503 sort_options options; 504 options.add_sort_option(sort_options::sample); 505 options.sort(symbols, false, false); 506 507 output_info(cout); 508 509 output_objdump_asm(symbols, app_name); 510 511 return true; 512 } 513 514 return false; 515 } 516 517 518 string const source_line_annotation(debug_name_id filename, size_t linenr) 519 { 520 string str; 521 522 count_array_t counts = samples->samples_count(filename, linenr); 523 if (!counts.zero()) { 524 str += count_str(counts, samples->samples_count()); 525 for (size_t i = 1; i < nr_events; ++i) 526 str += " "; 527 str += " :"; 528 } else { 529 str = annotation_fill; 530 } 531 532 return str; 533 } 534 535 536 string source_symbol_annotation(debug_name_id filename, size_t linenr) 537 { 538 symbol_collection const symbols = samples->find_symbol(filename, linenr); 539 540 if (symbols.empty()) 541 return string(); 542 543 string str = " " + begin_comment; 544 545 count_array_t counts; 546 for (size_t i = 0; i < symbols.size(); ++i) { 547 str += symbol_names.demangle(symbols[i]->name); 548 if (symbols.size() == 1) 549 str += " total: "; 550 else 551 str += " "; 552 str += count_str(symbols[i]->sample.counts, 553 samples->samples_count()); 554 if (symbols.size() != 1) 555 str += ", "; 556 557 counts += symbols[i]->sample.counts; 558 } 559 560 if (symbols.size() > 1) 561 str += "total: " + count_str(counts, samples->samples_count()); 562 str += end_comment; 563 564 return str; 565 } 566 567 568 void output_per_file_info(ostream & out, debug_name_id filename, 569 count_array_t const & total_file_count) 570 { 571 out << begin_comment << '\n' 572 << in_comment << "Total samples for file : " 573 << '"' << debug_names.name(filename) << '"' 574 << '\n'; 575 out << in_comment << '\n' << in_comment 576 << count_str(total_file_count, samples->samples_count()) 577 << '\n'; 578 out << end_comment << '\n' << '\n'; 579 } 580 581 582 string const line0_info(debug_name_id filename) 583 { 584 string annotation = source_line_annotation(filename, 0); 585 if (trim(annotation, " \t:").empty()) 586 return string(); 587 588 string str = "<credited to line zero> "; 589 str += annotation; 590 return str; 591 } 592 593 594 void do_output_one_file(ostream & out, istream & in, debug_name_id filename, 595 bool header) 596 { 597 count_array_t count = samples->samples_count(filename); 598 599 if (header) { 600 output_per_file_info(out, filename, count); 601 out << line0_info(filename) << '\n'; 602 } 603 604 605 if (in) { 606 string str; 607 608 for (size_t linenr = 1 ; getline(in, str) ; ++linenr) { 609 out << source_line_annotation(filename, linenr) << str 610 << source_symbol_annotation(filename, linenr) 611 << '\n'; 612 } 613 614 } else { 615 // source is not available but we can at least output all the 616 // symbols belonging to this file. This make more visible the 617 // problem of having less samples for a given file than the 618 // sum of all symbols samples for this file due to inlining 619 symbol_collection const symbols = samples->select_symbols(filename); 620 for (size_t i = 0; i < symbols.size(); ++i) 621 out << symbol_annotation(symbols[i]) << endl; 622 } 623 624 if (!header) { 625 output_per_file_info(out, filename, count); 626 out << line0_info(filename) << '\n'; 627 } 628 } 629 630 631 void output_one_file(istream & in, debug_name_id filename, 632 string const & source) 633 { 634 if (output_dir.empty()) { 635 do_output_one_file(cout, in, filename, true); 636 return; 637 } 638 639 string const out_file = op_realpath(output_dir + source); 640 641 /* Just because you're paranoid doesn't mean they're not out to 642 * get you ... 643 * 644 * This is just a lame final safety check. If we found the 645 * source, then "source" should be canonical already, and 646 * can't escape from the output dir. We can't use op_realpath() 647 * alone as that needs the file to exist already. 648 * 649 * Let's not complain again if we couldn't find the file anyway. 650 */ 651 if (out_file.find("/../") != string::npos) { 652 if (in) { 653 cerr << "refusing to create non-canonical filename " 654 << out_file << endl; 655 } 656 return; 657 } else if (!is_prefix(out_file, output_dir)) { 658 if (in) { 659 cerr << "refusing to create file " << out_file 660 << " outside of output directory " << output_dir 661 << endl; 662 } 663 return; 664 } 665 666 if (is_files_identical(out_file, source)) { 667 cerr << "input and output files are identical: " 668 << out_file << endl; 669 return; 670 } 671 672 if (create_path(out_file.c_str())) { 673 cerr << "unable to create file: " 674 << '"' << op_dirname(out_file) << '"' << endl; 675 return; 676 } 677 678 ofstream out(out_file.c_str()); 679 if (!out) { 680 cerr << "unable to open output file " 681 << '"' << out_file << '"' << endl; 682 } else { 683 do_output_one_file(out, in, filename, false); 684 output_info(out); 685 } 686 } 687 688 689 /* Locate a source file from debug info, which may be relative */ 690 string const locate_source_file(debug_name_id filename_id) 691 { 692 string const origfile = debug_names.name(filename_id); 693 string file = origfile; 694 695 if (file.empty()) 696 return file; 697 698 /* Allow absolute paths to be relocated to a different directory */ 699 if (file[0] == '/') { 700 vector<string>::const_iterator cit = base_dirs.begin(); 701 vector<string>::const_iterator end = base_dirs.end(); 702 for (; cit != end; ++cit) { 703 string path = op_realpath(*cit); 704 705 if (is_prefix(file, path)) { 706 file = file.substr(path.length()); 707 break; 708 } 709 } 710 } 711 712 vector<string>::const_iterator cit = search_dirs.begin(); 713 vector<string>::const_iterator end = search_dirs.end(); 714 715 for (; cit != end; ++cit) { 716 string const absfile = op_realpath(*cit + "/" + file); 717 718 if (op_file_readable(absfile)) 719 return absfile; 720 } 721 722 /* We didn't find a relocated absolute file, or a relative file, 723 * assume the original is correct, accounting for the 724 * possibility it's relative the cwd 725 */ 726 return op_realpath(origfile); 727 } 728 729 730 void output_source(path_filter const & filter) 731 { 732 bool const separate_file = !output_dir.empty(); 733 734 if (!separate_file) 735 output_info(cout); 736 737 vector<debug_name_id> filenames = 738 samples->select_filename(options::threshold); 739 740 for (size_t i = 0 ; i < filenames.size() ; ++i) { 741 string const & source = locate_source_file(filenames[i]); 742 743 if (!filter.match(source)) 744 continue; 745 746 ifstream in(source.c_str()); 747 748 // it is common to have empty filename due to the lack 749 // of debug info (eg _init function) so warn only 750 // if the filename is non empty. The case: no debug 751 // info at all has already been checked. 752 if (!in && source.length()) { 753 cerr << "opannotate (warning): unable to open for " 754 "reading: " << source << endl; 755 } 756 757 if (source.length()) 758 output_one_file(in, filenames[i], source); 759 } 760 } 761 762 763 bool annotate_source(list<string> const & images) 764 { 765 annotation_fill = get_annotation_fill(); 766 767 if (!output_dir.empty()) { 768 769 if (create_path(output_dir.c_str())) { 770 cerr << "unable to create " << output_dir 771 << " directory: " << endl; 772 return false; 773 } 774 775 // Make sure we have an absolute path. 776 output_dir = op_realpath(output_dir); 777 if (output_dir.length() && 778 output_dir[output_dir.length() - 1] != '/') 779 output_dir += '/'; 780 781 /* Don't let the user stomp on their sources */ 782 if (output_dir == "/") { 783 cerr << "Output path of / would over-write the " 784 "source files" << endl; 785 return false; 786 } 787 } 788 789 if (assembly) { 790 bool some_output = false; 791 792 list<string>::const_iterator it = images.begin(); 793 list<string>::const_iterator const end = images.end(); 794 795 for (; it != end; ++it) { 796 if (output_asm(*it)) 797 some_output = true; 798 } 799 800 if (!some_output) { 801 // It's the only case we must care since we know the 802 // selected image set is not empty 803 cerr << "selected image set doesn't contain any of " 804 << "the selected symbol\n"; 805 } 806 } else { 807 output_source(file_filter); 808 } 809 810 return true; 811 } 812 813 814 int opannotate(options::spec const & spec) 815 { 816 handle_options(spec); 817 818 nr_events = classes.v.size(); 819 820 samples.reset(new profile_container(true, true, 821 classes.extra_found_images)); 822 823 list<string> images; 824 825 list<inverted_profile> iprofiles = invert_profiles(classes); 826 827 report_image_errors(iprofiles, classes.extra_found_images); 828 829 list<inverted_profile>::iterator it = iprofiles.begin(); 830 list<inverted_profile>::iterator const end = iprofiles.end(); 831 832 bool debug_info = false; 833 for (; it != end; ++it) { 834 bool tmp = false; 835 populate_for_image(*samples, *it, 836 options::symbol_filter, &tmp); 837 images.push_back(it->image); 838 if (tmp) 839 debug_info = true; 840 } 841 842 if (!debug_info && !options::assembly) { 843 cerr << "opannotate (warning): no debug information available for binary " 844 << it->image << ", and --assembly not requested\n"; 845 } 846 847 annotate_source(images); 848 849 return 0; 850 } 851 852 } // anonymous namespace 853 854 855 int main(int argc, char const * argv[]) 856 { 857 // set the invocation, for the file headers later 858 for (int i = 0 ; i < argc ; ++i) 859 cmdline += string(argv[i]) + " "; 860 861 return run_pp_tool(argc, argv, opannotate); 862 } 863