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 <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:
     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::SetUpAndroidData(android_data_);
     63   }
     64 
     65   virtual ~AssemblerTestInfrastructure() {
     66     // We leave temporaries in case this failed so we can debug issues.
     67     CommonRuntimeTest::TearDownAndroidData(android_data_, false);
     68     tmpnam_ = "";
     69   }
     70 
     71   // This is intended to be run as a test.
     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).
    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.
    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.
    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.
    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.
    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.
    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.
    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 
    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 
    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 
    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.
    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.
    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.
    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 
    473   static std::string GetGCCRootPath() {
    474     return "prebuilts/gcc/linux-x86";
    475   }
    476 
    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 
    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.
    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.
    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.
    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