1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 #ifndef ART_COMPILER_UTILS_ASSEMBLER_TEST_BASE_H_ 18 #define ART_COMPILER_UTILS_ASSEMBLER_TEST_BASE_H_ 19 20 #include <sys/stat.h> 21 #include <cstdio> 22 #include <cstdlib> 23 #include <fstream> 24 #include <iterator> 25 26 #include "android-base/strings.h" 27 28 #include "base/utils.h" 29 #include "common_runtime_test.h" // For ScratchFile 30 #include "exec_utils.h" 31 32 namespace art { 33 34 // If you want to take a look at the differences between the ART assembler and GCC, set this flag 35 // to true. The disassembled files will then remain in the tmp directory. 36 static constexpr bool kKeepDisassembledFiles = false; 37 38 // Use a glocal static variable to keep the same name for all test data. Else we'll just spam the 39 // temp directory. 40 static std::string tmpnam_; // NOLINT [runtime/string] [4] 41 42 // We put this into a class as gtests are self-contained, so this helper needs to be in an h-file. 43 class AssemblerTestInfrastructure { 44 public: AssemblerTestInfrastructure(std::string architecture,std::string as,std::string as_params,std::string objdump,std::string objdump_params,std::string disasm,std::string disasm_params,const char * asm_header)45 AssemblerTestInfrastructure(std::string architecture, 46 std::string as, 47 std::string as_params, 48 std::string objdump, 49 std::string objdump_params, 50 std::string disasm, 51 std::string disasm_params, 52 const char* asm_header) : 53 architecture_string_(architecture), 54 asm_header_(asm_header), 55 assembler_cmd_name_(as), 56 assembler_parameters_(as_params), 57 objdump_cmd_name_(objdump), 58 objdump_parameters_(objdump_params), 59 disassembler_cmd_name_(disasm), 60 disassembler_parameters_(disasm_params) { 61 // Fake a runtime test for ScratchFile 62 CommonRuntimeTest::SetUpAndroidDataDir(android_data_); 63 } 64 ~AssemblerTestInfrastructure()65 virtual ~AssemblerTestInfrastructure() { 66 // We leave temporaries in case this failed so we can debug issues. 67 CommonRuntimeTest::TearDownAndroidDataDir(android_data_, false); 68 tmpnam_ = ""; 69 } 70 71 // This is intended to be run as a test. CheckTools()72 bool CheckTools() { 73 std::string asm_tool = FindTool(assembler_cmd_name_); 74 if (!FileExists(asm_tool)) { 75 LOG(ERROR) << "Could not find assembler from " << assembler_cmd_name_; 76 LOG(ERROR) << "FindTool returned " << asm_tool; 77 FindToolDump(assembler_cmd_name_); 78 return false; 79 } 80 LOG(INFO) << "Chosen assembler command: " << GetAssemblerCommand(); 81 82 std::string objdump_tool = FindTool(objdump_cmd_name_); 83 if (!FileExists(objdump_tool)) { 84 LOG(ERROR) << "Could not find objdump from " << objdump_cmd_name_; 85 LOG(ERROR) << "FindTool returned " << objdump_tool; 86 FindToolDump(objdump_cmd_name_); 87 return false; 88 } 89 LOG(INFO) << "Chosen objdump command: " << GetObjdumpCommand(); 90 91 // Disassembly is optional. 92 std::string disassembler = GetDisassembleCommand(); 93 if (disassembler.length() != 0) { 94 std::string disassembler_tool = FindTool(disassembler_cmd_name_); 95 if (!FileExists(disassembler_tool)) { 96 LOG(ERROR) << "Could not find disassembler from " << disassembler_cmd_name_; 97 LOG(ERROR) << "FindTool returned " << disassembler_tool; 98 FindToolDump(disassembler_cmd_name_); 99 return false; 100 } 101 LOG(INFO) << "Chosen disassemble command: " << GetDisassembleCommand(); 102 } else { 103 LOG(INFO) << "No disassembler given."; 104 } 105 106 return true; 107 } 108 109 // Driver() assembles and compares the results. If the results are not equal and we have a 110 // disassembler, disassemble both and check whether they have the same mnemonics (in which case 111 // we just warn). Driver(const std::vector<uint8_t> & data,const std::string & assembly_text,const std::string & test_name)112 void Driver(const std::vector<uint8_t>& data, 113 const std::string& assembly_text, 114 const std::string& test_name) { 115 EXPECT_NE(assembly_text.length(), 0U) << "Empty assembly"; 116 117 NativeAssemblerResult res; 118 Compile(assembly_text, &res, test_name); 119 120 EXPECT_TRUE(res.ok) << res.error_msg; 121 if (!res.ok) { 122 // No way of continuing. 123 return; 124 } 125 126 if (data == *res.code) { 127 Clean(&res); 128 } else { 129 if (DisassembleBinaries(data, *res.code, test_name)) { 130 if (data.size() > res.code->size()) { 131 // Fail this test with a fancy colored warning being printed. 132 EXPECT_TRUE(false) << "Assembly code is not identical, but disassembly of machine code " 133 "is equal: this implies sub-optimal encoding! Our code size=" << data.size() << 134 ", gcc size=" << res.code->size(); 135 } else { 136 // Otherwise just print an info message and clean up. 137 LOG(INFO) << "GCC chose a different encoding than ours, but the overall length is the " 138 "same."; 139 Clean(&res); 140 } 141 } else { 142 // This will output the assembly. 143 EXPECT_EQ(*res.code, data) << "Outputs (and disassembly) not identical."; 144 } 145 } 146 } 147 148 protected: 149 // Return the host assembler command for this test. GetAssemblerCommand()150 virtual std::string GetAssemblerCommand() { 151 // Already resolved it once? 152 if (resolved_assembler_cmd_.length() != 0) { 153 return resolved_assembler_cmd_; 154 } 155 156 std::string line = FindTool(assembler_cmd_name_); 157 if (line.length() == 0) { 158 return line; 159 } 160 161 resolved_assembler_cmd_ = line + assembler_parameters_; 162 163 return resolved_assembler_cmd_; 164 } 165 166 // Return the host objdump command for this test. GetObjdumpCommand()167 virtual std::string GetObjdumpCommand() { 168 // Already resolved it once? 169 if (resolved_objdump_cmd_.length() != 0) { 170 return resolved_objdump_cmd_; 171 } 172 173 std::string line = FindTool(objdump_cmd_name_); 174 if (line.length() == 0) { 175 return line; 176 } 177 178 resolved_objdump_cmd_ = line + objdump_parameters_; 179 180 return resolved_objdump_cmd_; 181 } 182 183 // Return the host disassembler command for this test. GetDisassembleCommand()184 virtual std::string GetDisassembleCommand() { 185 // Already resolved it once? 186 if (resolved_disassemble_cmd_.length() != 0) { 187 return resolved_disassemble_cmd_; 188 } 189 190 std::string line = FindTool(disassembler_cmd_name_); 191 if (line.length() == 0) { 192 return line; 193 } 194 195 resolved_disassemble_cmd_ = line + disassembler_parameters_; 196 197 return resolved_disassemble_cmd_; 198 } 199 200 private: 201 // Structure to store intermediates and results. 202 struct NativeAssemblerResult { 203 bool ok; 204 std::string error_msg; 205 std::string base_name; 206 std::unique_ptr<std::vector<uint8_t>> code; 207 uintptr_t length; 208 }; 209 210 // Compile the assembly file from_file to a binary file to_file. Returns true on success. Assemble(const char * from_file,const char * to_file,std::string * error_msg)211 bool Assemble(const char* from_file, const char* to_file, std::string* error_msg) { 212 bool have_assembler = FileExists(FindTool(assembler_cmd_name_)); 213 EXPECT_TRUE(have_assembler) << "Cannot find assembler:" << GetAssemblerCommand(); 214 if (!have_assembler) { 215 return false; 216 } 217 218 std::vector<std::string> args; 219 220 // Encaspulate the whole command line in a single string passed to 221 // the shell, so that GetAssemblerCommand() may contain arguments 222 // in addition to the program name. 223 args.push_back(GetAssemblerCommand()); 224 args.push_back("-o"); 225 args.push_back(to_file); 226 args.push_back(from_file); 227 std::string cmd = android::base::Join(args, ' '); 228 229 args.clear(); 230 args.push_back("/bin/sh"); 231 args.push_back("-c"); 232 args.push_back(cmd); 233 234 bool success = Exec(args, error_msg); 235 if (!success) { 236 LOG(ERROR) << "Assembler command line:"; 237 for (const std::string& arg : args) { 238 LOG(ERROR) << arg; 239 } 240 } 241 return success; 242 } 243 244 // Runs objdump -h on the binary file and extracts the first line with .text. 245 // Returns "" on failure. Objdump(const std::string & file)246 std::string Objdump(const std::string& file) { 247 bool have_objdump = FileExists(FindTool(objdump_cmd_name_)); 248 EXPECT_TRUE(have_objdump) << "Cannot find objdump: " << GetObjdumpCommand(); 249 if (!have_objdump) { 250 return ""; 251 } 252 253 std::string error_msg; 254 std::vector<std::string> args; 255 256 // Encaspulate the whole command line in a single string passed to 257 // the shell, so that GetObjdumpCommand() may contain arguments 258 // in addition to the program name. 259 args.push_back(GetObjdumpCommand()); 260 args.push_back(file); 261 args.push_back(">"); 262 args.push_back(file+".dump"); 263 std::string cmd = android::base::Join(args, ' '); 264 265 args.clear(); 266 args.push_back("/bin/sh"); 267 args.push_back("-c"); 268 args.push_back(cmd); 269 270 if (!Exec(args, &error_msg)) { 271 EXPECT_TRUE(false) << error_msg; 272 } 273 274 std::ifstream dump(file+".dump"); 275 276 std::string line; 277 bool found = false; 278 while (std::getline(dump, line)) { 279 if (line.find(".text") != line.npos) { 280 found = true; 281 break; 282 } 283 } 284 285 dump.close(); 286 287 if (found) { 288 return line; 289 } else { 290 return ""; 291 } 292 } 293 294 // Disassemble both binaries and compare the text. DisassembleBinaries(const std::vector<uint8_t> & data,const std::vector<uint8_t> & as,const std::string & test_name)295 bool DisassembleBinaries(const std::vector<uint8_t>& data, 296 const std::vector<uint8_t>& as, 297 const std::string& test_name) { 298 std::string disassembler = GetDisassembleCommand(); 299 if (disassembler.length() == 0) { 300 LOG(WARNING) << "No dissassembler command."; 301 return false; 302 } 303 304 std::string data_name = WriteToFile(data, test_name + ".ass"); 305 std::string error_msg; 306 if (!DisassembleBinary(data_name, &error_msg)) { 307 LOG(INFO) << "Error disassembling: " << error_msg; 308 std::remove(data_name.c_str()); 309 return false; 310 } 311 312 std::string as_name = WriteToFile(as, test_name + ".gcc"); 313 if (!DisassembleBinary(as_name, &error_msg)) { 314 LOG(INFO) << "Error disassembling: " << error_msg; 315 std::remove(data_name.c_str()); 316 std::remove((data_name + ".dis").c_str()); 317 std::remove(as_name.c_str()); 318 return false; 319 } 320 321 bool result = CompareFiles(data_name + ".dis", as_name + ".dis"); 322 323 if (!kKeepDisassembledFiles) { 324 std::remove(data_name.c_str()); 325 std::remove(as_name.c_str()); 326 std::remove((data_name + ".dis").c_str()); 327 std::remove((as_name + ".dis").c_str()); 328 } 329 330 return result; 331 } 332 DisassembleBinary(const std::string & file,std::string * error_msg)333 bool DisassembleBinary(const std::string& file, std::string* error_msg) { 334 std::vector<std::string> args; 335 336 // Encaspulate the whole command line in a single string passed to 337 // the shell, so that GetDisassembleCommand() may contain arguments 338 // in addition to the program name. 339 args.push_back(GetDisassembleCommand()); 340 args.push_back(file); 341 args.push_back("| sed -n \'/<.data>/,$p\' | sed -e \'s/.*://\'"); 342 args.push_back(">"); 343 args.push_back(file+".dis"); 344 std::string cmd = android::base::Join(args, ' '); 345 346 args.clear(); 347 args.push_back("/bin/sh"); 348 args.push_back("-c"); 349 args.push_back(cmd); 350 351 return Exec(args, error_msg); 352 } 353 WriteToFile(const std::vector<uint8_t> & buffer,const std::string & test_name)354 std::string WriteToFile(const std::vector<uint8_t>& buffer, const std::string& test_name) { 355 std::string file_name = GetTmpnam() + std::string("---") + test_name; 356 const char* data = reinterpret_cast<const char*>(buffer.data()); 357 std::ofstream s_out(file_name + ".o"); 358 s_out.write(data, buffer.size()); 359 s_out.close(); 360 return file_name + ".o"; 361 } 362 CompareFiles(const std::string & f1,const std::string & f2)363 bool CompareFiles(const std::string& f1, const std::string& f2) { 364 std::ifstream f1_in(f1); 365 std::ifstream f2_in(f2); 366 367 bool result = std::equal(std::istreambuf_iterator<char>(f1_in), 368 std::istreambuf_iterator<char>(), 369 std::istreambuf_iterator<char>(f2_in)); 370 371 f1_in.close(); 372 f2_in.close(); 373 374 return result; 375 } 376 377 // Compile the given assembly code and extract the binary, if possible. Put result into res. Compile(const std::string & assembly_code,NativeAssemblerResult * res,const std::string & test_name)378 bool Compile(const std::string& assembly_code, 379 NativeAssemblerResult* res, 380 const std::string& test_name) { 381 res->ok = false; 382 res->code.reset(nullptr); 383 384 res->base_name = GetTmpnam() + std::string("---") + test_name; 385 386 // TODO: Lots of error checking. 387 388 std::ofstream s_out(res->base_name + ".S"); 389 if (asm_header_ != nullptr) { 390 s_out << asm_header_; 391 } 392 s_out << assembly_code; 393 s_out.close(); 394 395 if (!Assemble((res->base_name + ".S").c_str(), (res->base_name + ".o").c_str(), 396 &res->error_msg)) { 397 res->error_msg = "Could not compile."; 398 return false; 399 } 400 401 std::string odump = Objdump(res->base_name + ".o"); 402 if (odump.length() == 0) { 403 res->error_msg = "Objdump failed."; 404 return false; 405 } 406 407 std::istringstream iss(odump); 408 std::istream_iterator<std::string> start(iss); 409 std::istream_iterator<std::string> end; 410 std::vector<std::string> tokens(start, end); 411 412 if (tokens.size() < OBJDUMP_SECTION_LINE_MIN_TOKENS) { 413 res->error_msg = "Objdump output not recognized: too few tokens."; 414 return false; 415 } 416 417 if (tokens[1] != ".text") { 418 res->error_msg = "Objdump output not recognized: .text not second token."; 419 return false; 420 } 421 422 std::string lengthToken = "0x" + tokens[2]; 423 std::istringstream(lengthToken) >> std::hex >> res->length; 424 425 std::string offsetToken = "0x" + tokens[5]; 426 uintptr_t offset; 427 std::istringstream(offsetToken) >> std::hex >> offset; 428 429 std::ifstream obj(res->base_name + ".o"); 430 obj.seekg(offset); 431 res->code.reset(new std::vector<uint8_t>(res->length)); 432 obj.read(reinterpret_cast<char*>(&(*res->code)[0]), res->length); 433 obj.close(); 434 435 res->ok = true; 436 return true; 437 } 438 439 // Remove temporary files. Clean(const NativeAssemblerResult * res)440 void Clean(const NativeAssemblerResult* res) { 441 std::remove((res->base_name + ".S").c_str()); 442 std::remove((res->base_name + ".o").c_str()); 443 std::remove((res->base_name + ".o.dump").c_str()); 444 } 445 446 // Check whether file exists. Is used for commands, so strips off any parameters: anything after 447 // the first space. We skip to the last slash for this, so it should work with directories with 448 // spaces. FileExists(const std::string & file)449 static bool FileExists(const std::string& file) { 450 if (file.length() == 0) { 451 return false; 452 } 453 454 // Need to strip any options. 455 size_t last_slash = file.find_last_of('/'); 456 if (last_slash == std::string::npos) { 457 // No slash, start looking at the start. 458 last_slash = 0; 459 } 460 size_t space_index = file.find(' ', last_slash); 461 462 if (space_index == std::string::npos) { 463 std::ifstream infile(file.c_str()); 464 return infile.good(); 465 } else { 466 std::string copy = file.substr(0, space_index - 1); 467 468 struct stat buf; 469 return stat(copy.c_str(), &buf) == 0; 470 } 471 } 472 GetGCCRootPath()473 static std::string GetGCCRootPath() { 474 return "prebuilts/gcc/linux-x86"; 475 } 476 GetRootPath()477 static std::string GetRootPath() { 478 // 1) Check ANDROID_BUILD_TOP 479 char* build_top = getenv("ANDROID_BUILD_TOP"); 480 if (build_top != nullptr) { 481 return std::string(build_top) + "/"; 482 } 483 484 // 2) Do cwd 485 char temp[1024]; 486 return getcwd(temp, 1024) ? std::string(temp) + "/" : std::string(""); 487 } 488 FindTool(const std::string & tool_name)489 std::string FindTool(const std::string& tool_name) { 490 // Find the current tool. Wild-card pattern is "arch-string*tool-name". 491 std::string gcc_path = GetRootPath() + GetGCCRootPath(); 492 std::vector<std::string> args; 493 args.push_back("find"); 494 args.push_back(gcc_path); 495 args.push_back("-name"); 496 args.push_back(architecture_string_ + "*" + tool_name); 497 args.push_back("|"); 498 args.push_back("sort"); 499 args.push_back("|"); 500 args.push_back("tail"); 501 args.push_back("-n"); 502 args.push_back("1"); 503 std::string tmp_file = GetTmpnam(); 504 args.push_back(">"); 505 args.push_back(tmp_file); 506 std::string sh_args = android::base::Join(args, ' '); 507 508 args.clear(); 509 args.push_back("/bin/sh"); 510 args.push_back("-c"); 511 args.push_back(sh_args); 512 513 std::string error_msg; 514 if (!Exec(args, &error_msg)) { 515 EXPECT_TRUE(false) << error_msg; 516 UNREACHABLE(); 517 } 518 519 std::ifstream in(tmp_file.c_str()); 520 std::string line; 521 if (!std::getline(in, line)) { 522 in.close(); 523 std::remove(tmp_file.c_str()); 524 return ""; 525 } 526 in.close(); 527 std::remove(tmp_file.c_str()); 528 return line; 529 } 530 531 // Helper for below. If name_predicate is empty, search for all files, otherwise use it for the 532 // "-name" option. FindToolDumpPrintout(const std::string & name_predicate,const std::string & tmp_file)533 static void FindToolDumpPrintout(const std::string& name_predicate, 534 const std::string& tmp_file) { 535 std::string gcc_path = GetRootPath() + GetGCCRootPath(); 536 std::vector<std::string> args; 537 args.push_back("find"); 538 args.push_back(gcc_path); 539 if (!name_predicate.empty()) { 540 args.push_back("-name"); 541 args.push_back(name_predicate); 542 } 543 args.push_back("|"); 544 args.push_back("sort"); 545 args.push_back(">"); 546 args.push_back(tmp_file); 547 std::string sh_args = android::base::Join(args, ' '); 548 549 args.clear(); 550 args.push_back("/bin/sh"); 551 args.push_back("-c"); 552 args.push_back(sh_args); 553 554 std::string error_msg; 555 if (!Exec(args, &error_msg)) { 556 EXPECT_TRUE(false) << error_msg; 557 UNREACHABLE(); 558 } 559 560 LOG(ERROR) << "FindToolDump: gcc_path=" << gcc_path 561 << " cmd=" << sh_args; 562 std::ifstream in(tmp_file.c_str()); 563 if (in) { 564 std::string line; 565 while (std::getline(in, line)) { 566 LOG(ERROR) << line; 567 } 568 } 569 in.close(); 570 std::remove(tmp_file.c_str()); 571 } 572 573 // For debug purposes. FindToolDump(const std::string & tool_name)574 void FindToolDump(const std::string& tool_name) { 575 // Check with the tool name. 576 FindToolDumpPrintout(architecture_string_ + "*" + tool_name, GetTmpnam()); 577 FindToolDumpPrintout("", GetTmpnam()); 578 } 579 580 // Use a consistent tmpnam, so store it. GetTmpnam()581 std::string GetTmpnam() { 582 if (tmpnam_.length() == 0) { 583 ScratchFile tmp; 584 tmpnam_ = tmp.GetFilename() + "asm"; 585 } 586 return tmpnam_; 587 } 588 589 static constexpr size_t OBJDUMP_SECTION_LINE_MIN_TOKENS = 6; 590 591 std::string architecture_string_; 592 const char* asm_header_; 593 594 std::string assembler_cmd_name_; 595 std::string assembler_parameters_; 596 597 std::string objdump_cmd_name_; 598 std::string objdump_parameters_; 599 600 std::string disassembler_cmd_name_; 601 std::string disassembler_parameters_; 602 603 std::string resolved_assembler_cmd_; 604 std::string resolved_objdump_cmd_; 605 std::string resolved_disassemble_cmd_; 606 607 std::string android_data_; 608 609 DISALLOW_COPY_AND_ASSIGN(AssemblerTestInfrastructure); 610 }; 611 612 } // namespace art 613 614 #endif // ART_COMPILER_UTILS_ASSEMBLER_TEST_BASE_H_ 615