/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef ART_COMPILER_UTILS_ASSEMBLER_TEST_BASE_H_
#define ART_COMPILER_UTILS_ASSEMBLER_TEST_BASE_H_
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <iterator>
#include <sys/stat.h>
#include "common_runtime_test.h" // For ScratchFile
#include "utils.h"
namespace art {
// If you want to take a look at the differences between the ART assembler and GCC, set this flag
// to true. The disassembled files will then remain in the tmp directory.
static constexpr bool kKeepDisassembledFiles = false;
// Use a glocal static variable to keep the same name for all test data. Else we'll just spam the
// temp directory.
static std::string tmpnam_;
// We put this into a class as gtests are self-contained, so this helper needs to be in an h-file.
class AssemblerTestInfrastructure {
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) :
architecture_string_(architecture),
asm_header_(asm_header),
assembler_cmd_name_(as),
assembler_parameters_(as_params),
objdump_cmd_name_(objdump),
objdump_parameters_(objdump_params),
disassembler_cmd_name_(disasm),
disassembler_parameters_(disasm_params) {
// Fake a runtime test for ScratchFile
CommonRuntimeTest::SetUpAndroidData(android_data_);
}
virtual ~AssemblerTestInfrastructure() {
// We leave temporaries in case this failed so we can debug issues.
CommonRuntimeTest::TearDownAndroidData(android_data_, false);
tmpnam_ = "";
}
// This is intended to be run as a test.
bool CheckTools() {
std::string asm_tool = FindTool(assembler_cmd_name_);
if (!FileExists(asm_tool)) {
LOG(ERROR) << "Could not find assembler from " << assembler_cmd_name_;
LOG(ERROR) << "FindTool returned " << asm_tool;
FindToolDump(assembler_cmd_name_);
return false;
}
LOG(INFO) << "Chosen assembler command: " << GetAssemblerCommand();
std::string objdump_tool = FindTool(objdump_cmd_name_);
if (!FileExists(objdump_tool)) {
LOG(ERROR) << "Could not find objdump from " << objdump_cmd_name_;
LOG(ERROR) << "FindTool returned " << objdump_tool;
FindToolDump(objdump_cmd_name_);
return false;
}
LOG(INFO) << "Chosen objdump command: " << GetObjdumpCommand();
// Disassembly is optional.
std::string disassembler = GetDisassembleCommand();
if (disassembler.length() != 0) {
std::string disassembler_tool = FindTool(disassembler_cmd_name_);
if (!FileExists(disassembler_tool)) {
LOG(ERROR) << "Could not find disassembler from " << disassembler_cmd_name_;
LOG(ERROR) << "FindTool returned " << disassembler_tool;
FindToolDump(disassembler_cmd_name_);
return false;
}
LOG(INFO) << "Chosen disassemble command: " << GetDisassembleCommand();
} else {
LOG(INFO) << "No disassembler given.";
}
return true;
}
// Driver() assembles and compares the results. If the results are not equal and we have a
// disassembler, disassemble both and check whether they have the same mnemonics (in which case
// we just warn).
void Driver(const std::vector<uint8_t>& data, std::string assembly_text, std::string test_name) {
EXPECT_NE(assembly_text.length(), 0U) << "Empty assembly";
NativeAssemblerResult res;
Compile(assembly_text, &res, test_name);
EXPECT_TRUE(res.ok) << res.error_msg;
if (!res.ok) {
// No way of continuing.
return;
}
if (data == *res.code) {
Clean(&res);
} else {
if (DisassembleBinaries(data, *res.code, test_name)) {
if (data.size() > res.code->size()) {
// Fail this test with a fancy colored warning being printed.
EXPECT_TRUE(false) << "Assembly code is not identical, but disassembly of machine code "
"is equal: this implies sub-optimal encoding! Our code size=" << data.size() <<
", gcc size=" << res.code->size();
} else {
// Otherwise just print an info message and clean up.
LOG(INFO) << "GCC chose a different encoding than ours, but the overall length is the "
"same.";
Clean(&res);
}
} else {
// This will output the assembly.
EXPECT_EQ(*res.code, data) << "Outputs (and disassembly) not identical.";
}
}
}
protected:
// Return the host assembler command for this test.
virtual std::string GetAssemblerCommand() {
// Already resolved it once?
if (resolved_assembler_cmd_.length() != 0) {
return resolved_assembler_cmd_;
}
std::string line = FindTool(assembler_cmd_name_);
if (line.length() == 0) {
return line;
}
resolved_assembler_cmd_ = line + assembler_parameters_;
return resolved_assembler_cmd_;
}
// Return the host objdump command for this test.
virtual std::string GetObjdumpCommand() {
// Already resolved it once?
if (resolved_objdump_cmd_.length() != 0) {
return resolved_objdump_cmd_;
}
std::string line = FindTool(objdump_cmd_name_);
if (line.length() == 0) {
return line;
}
resolved_objdump_cmd_ = line + objdump_parameters_;
return resolved_objdump_cmd_;
}
// Return the host disassembler command for this test.
virtual std::string GetDisassembleCommand() {
// Already resolved it once?
if (resolved_disassemble_cmd_.length() != 0) {
return resolved_disassemble_cmd_;
}
std::string line = FindTool(disassembler_cmd_name_);
if (line.length() == 0) {
return line;
}
resolved_disassemble_cmd_ = line + disassembler_parameters_;
return resolved_disassemble_cmd_;
}
private:
// Structure to store intermediates and results.
struct NativeAssemblerResult {
bool ok;
std::string error_msg;
std::string base_name;
std::unique_ptr<std::vector<uint8_t>> code;
uintptr_t length;
};
// Compile the assembly file from_file to a binary file to_file. Returns true on success.
bool Assemble(const char* from_file, const char* to_file, std::string* error_msg) {
bool have_assembler = FileExists(FindTool(assembler_cmd_name_));
EXPECT_TRUE(have_assembler) << "Cannot find assembler:" << GetAssemblerCommand();
if (!have_assembler) {
return false;
}
std::vector<std::string> args;
// Encaspulate the whole command line in a single string passed to
// the shell, so that GetAssemblerCommand() may contain arguments
// in addition to the program name.
args.push_back(GetAssemblerCommand());
args.push_back("-o");
args.push_back(to_file);
args.push_back(from_file);
std::string cmd = Join(args, ' ');
args.clear();
args.push_back("/bin/sh");
args.push_back("-c");
args.push_back(cmd);
bool success = Exec(args, error_msg);
if (!success) {
LOG(ERROR) << "Assembler command line:";
for (std::string arg : args) {
LOG(ERROR) << arg;
}
}
return success;
}
// Runs objdump -h on the binary file and extracts the first line with .text.
// Returns "" on failure.
std::string Objdump(std::string file) {
bool have_objdump = FileExists(FindTool(objdump_cmd_name_));
EXPECT_TRUE(have_objdump) << "Cannot find objdump: " << GetObjdumpCommand();
if (!have_objdump) {
return "";
}
std::string error_msg;
std::vector<std::string> args;
// Encaspulate the whole command line in a single string passed to
// the shell, so that GetObjdumpCommand() may contain arguments
// in addition to the program name.
args.push_back(GetObjdumpCommand());
args.push_back(file);
args.push_back(">");
args.push_back(file+".dump");
std::string cmd = Join(args, ' ');
args.clear();
args.push_back("/bin/sh");
args.push_back("-c");
args.push_back(cmd);
if (!Exec(args, &error_msg)) {
EXPECT_TRUE(false) << error_msg;
}
std::ifstream dump(file+".dump");
std::string line;
bool found = false;
while (std::getline(dump, line)) {
if (line.find(".text") != line.npos) {
found = true;
break;
}
}
dump.close();
if (found) {
return line;
} else {
return "";
}
}
// Disassemble both binaries and compare the text.
bool DisassembleBinaries(const std::vector<uint8_t>& data, const std::vector<uint8_t>& as,
std::string test_name) {
std::string disassembler = GetDisassembleCommand();
if (disassembler.length() == 0) {
LOG(WARNING) << "No dissassembler command.";
return false;
}
std::string data_name = WriteToFile(data, test_name + ".ass");
std::string error_msg;
if (!DisassembleBinary(data_name, &error_msg)) {
LOG(INFO) << "Error disassembling: " << error_msg;
std::remove(data_name.c_str());
return false;
}
std::string as_name = WriteToFile(as, test_name + ".gcc");
if (!DisassembleBinary(as_name, &error_msg)) {
LOG(INFO) << "Error disassembling: " << error_msg;
std::remove(data_name.c_str());
std::remove((data_name + ".dis").c_str());
std::remove(as_name.c_str());
return false;
}
bool result = CompareFiles(data_name + ".dis", as_name + ".dis");
if (!kKeepDisassembledFiles) {
std::remove(data_name.c_str());
std::remove(as_name.c_str());
std::remove((data_name + ".dis").c_str());
std::remove((as_name + ".dis").c_str());
}
return result;
}
bool DisassembleBinary(std::string file, std::string* error_msg) {
std::vector<std::string> args;
// Encaspulate the whole command line in a single string passed to
// the shell, so that GetDisassembleCommand() may contain arguments
// in addition to the program name.
args.push_back(GetDisassembleCommand());
args.push_back(file);
args.push_back("| sed -n \'/<.data>/,$p\' | sed -e \'s/.*://\'");
args.push_back(">");
args.push_back(file+".dis");
std::string cmd = Join(args, ' ');
args.clear();
args.push_back("/bin/sh");
args.push_back("-c");
args.push_back(cmd);
return Exec(args, error_msg);
}
std::string WriteToFile(const std::vector<uint8_t>& buffer, std::string test_name) {
std::string file_name = GetTmpnam() + std::string("---") + test_name;
const char* data = reinterpret_cast<const char*>(buffer.data());
std::ofstream s_out(file_name + ".o");
s_out.write(data, buffer.size());
s_out.close();
return file_name + ".o";
}
bool CompareFiles(std::string f1, std::string f2) {
std::ifstream f1_in(f1);
std::ifstream f2_in(f2);
bool result = std::equal(std::istreambuf_iterator<char>(f1_in),
std::istreambuf_iterator<char>(),
std::istreambuf_iterator<char>(f2_in));
f1_in.close();
f2_in.close();
return result;
}
// Compile the given assembly code and extract the binary, if possible. Put result into res.
bool Compile(std::string assembly_code, NativeAssemblerResult* res, std::string test_name) {
res->ok = false;
res->code.reset(nullptr);
res->base_name = GetTmpnam() + std::string("---") + test_name;
// TODO: Lots of error checking.
std::ofstream s_out(res->base_name + ".S");
if (asm_header_ != nullptr) {
s_out << asm_header_;
}
s_out << assembly_code;
s_out.close();
if (!Assemble((res->base_name + ".S").c_str(), (res->base_name + ".o").c_str(),
&res->error_msg)) {
res->error_msg = "Could not compile.";
return false;
}
std::string odump = Objdump(res->base_name + ".o");
if (odump.length() == 0) {
res->error_msg = "Objdump failed.";
return false;
}
std::istringstream iss(odump);
std::istream_iterator<std::string> start(iss);
std::istream_iterator<std::string> end;
std::vector<std::string> tokens(start, end);
if (tokens.size() < OBJDUMP_SECTION_LINE_MIN_TOKENS) {
res->error_msg = "Objdump output not recognized: too few tokens.";
return false;
}
if (tokens[1] != ".text") {
res->error_msg = "Objdump output not recognized: .text not second token.";
return false;
}
std::string lengthToken = "0x" + tokens[2];
std::istringstream(lengthToken) >> std::hex >> res->length;
std::string offsetToken = "0x" + tokens[5];
uintptr_t offset;
std::istringstream(offsetToken) >> std::hex >> offset;
std::ifstream obj(res->base_name + ".o");
obj.seekg(offset);
res->code.reset(new std::vector<uint8_t>(res->length));
obj.read(reinterpret_cast<char*>(&(*res->code)[0]), res->length);
obj.close();
res->ok = true;
return true;
}
// Remove temporary files.
void Clean(const NativeAssemblerResult* res) {
std::remove((res->base_name + ".S").c_str());
std::remove((res->base_name + ".o").c_str());
std::remove((res->base_name + ".o.dump").c_str());
}
// Check whether file exists. Is used for commands, so strips off any parameters: anything after
// the first space. We skip to the last slash for this, so it should work with directories with
// spaces.
static bool FileExists(std::string file) {
if (file.length() == 0) {
return false;
}
// Need to strip any options.
size_t last_slash = file.find_last_of('/');
if (last_slash == std::string::npos) {
// No slash, start looking at the start.
last_slash = 0;
}
size_t space_index = file.find(' ', last_slash);
if (space_index == std::string::npos) {
std::ifstream infile(file.c_str());
return infile.good();
} else {
std::string copy = file.substr(0, space_index - 1);
struct stat buf;
return stat(copy.c_str(), &buf) == 0;
}
}
static std::string GetGCCRootPath() {
return "prebuilts/gcc/linux-x86";
}
static std::string GetRootPath() {
// 1) Check ANDROID_BUILD_TOP
char* build_top = getenv("ANDROID_BUILD_TOP");
if (build_top != nullptr) {
return std::string(build_top) + "/";
}
// 2) Do cwd
char temp[1024];
return getcwd(temp, 1024) ? std::string(temp) + "/" : std::string("");
}
std::string FindTool(std::string tool_name) {
// Find the current tool. Wild-card pattern is "arch-string*tool-name".
std::string gcc_path = GetRootPath() + GetGCCRootPath();
std::vector<std::string> args;
args.push_back("find");
args.push_back(gcc_path);
args.push_back("-name");
args.push_back(architecture_string_ + "*" + tool_name);
args.push_back("|");
args.push_back("sort");
args.push_back("|");
args.push_back("tail");
args.push_back("-n");
args.push_back("1");
std::string tmp_file = GetTmpnam();
args.push_back(">");
args.push_back(tmp_file);
std::string sh_args = Join(args, ' ');
args.clear();
args.push_back("/bin/sh");
args.push_back("-c");
args.push_back(sh_args);
std::string error_msg;
if (!Exec(args, &error_msg)) {
EXPECT_TRUE(false) << error_msg;
UNREACHABLE();
}
std::ifstream in(tmp_file.c_str());
std::string line;
if (!std::getline(in, line)) {
in.close();
std::remove(tmp_file.c_str());
return "";
}
in.close();
std::remove(tmp_file.c_str());
return line;
}
// Helper for below. If name_predicate is empty, search for all files, otherwise use it for the
// "-name" option.
static void FindToolDumpPrintout(std::string name_predicate, std::string tmp_file) {
std::string gcc_path = GetRootPath() + GetGCCRootPath();
std::vector<std::string> args;
args.push_back("find");
args.push_back(gcc_path);
if (!name_predicate.empty()) {
args.push_back("-name");
args.push_back(name_predicate);
}
args.push_back("|");
args.push_back("sort");
args.push_back(">");
args.push_back(tmp_file);
std::string sh_args = Join(args, ' ');
args.clear();
args.push_back("/bin/sh");
args.push_back("-c");
args.push_back(sh_args);
std::string error_msg;
if (!Exec(args, &error_msg)) {
EXPECT_TRUE(false) << error_msg;
UNREACHABLE();
}
LOG(ERROR) << "FindToolDump: gcc_path=" << gcc_path
<< " cmd=" << sh_args;
std::ifstream in(tmp_file.c_str());
if (in) {
std::string line;
while (std::getline(in, line)) {
LOG(ERROR) << line;
}
}
in.close();
std::remove(tmp_file.c_str());
}
// For debug purposes.
void FindToolDump(std::string tool_name) {
// Check with the tool name.
FindToolDumpPrintout(architecture_string_ + "*" + tool_name, GetTmpnam());
FindToolDumpPrintout("", GetTmpnam());
}
// Use a consistent tmpnam, so store it.
std::string GetTmpnam() {
if (tmpnam_.length() == 0) {
ScratchFile tmp;
tmpnam_ = tmp.GetFilename() + "asm";
}
return tmpnam_;
}
static constexpr size_t OBJDUMP_SECTION_LINE_MIN_TOKENS = 6;
std::string architecture_string_;
const char* asm_header_;
std::string assembler_cmd_name_;
std::string assembler_parameters_;
std::string objdump_cmd_name_;
std::string objdump_parameters_;
std::string disassembler_cmd_name_;
std::string disassembler_parameters_;
std::string resolved_assembler_cmd_;
std::string resolved_objdump_cmd_;
std::string resolved_disassemble_cmd_;
std::string android_data_;
DISALLOW_COPY_AND_ASSIGN(AssemblerTestInfrastructure);
};
} // namespace art
#endif // ART_COMPILER_UTILS_ASSEMBLER_TEST_BASE_H_