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::SetUpAndroidData(android_data_);
63   }
64 
~AssemblerTestInfrastructure()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.
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