Home | History | Annotate | Download | only in utils
      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:
     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 
     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.
     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).
    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.
    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.
    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.
    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.
    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.
    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.
    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 
    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 
    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 
    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.
    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.
    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.
    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 
    465   static std::string GetGCCRootPath() {
    466     return "prebuilts/gcc/linux-x86";
    467   }
    468 
    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 
    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.
    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.
    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.
    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