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