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
29namespace 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.
33static 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.
37static 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.
40class 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