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