614 lines
18 KiB
C++
614 lines
18 KiB
C++
/*
|
|
* 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 "android-base/strings.h"
|
|
|
|
#include "common_runtime_test.h" // For ScratchFile
|
|
#include "exec_utils.h"
|
|
#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,
|
|
const std::string& assembly_text,
|
|
const 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 = android::base::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 (const 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(const 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 = android::base::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,
|
|
const 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(const 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 = android::base::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, const 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(const std::string& f1, const 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(const std::string& assembly_code,
|
|
NativeAssemblerResult* res,
|
|
const 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(const 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(const 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 = android::base::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(const std::string& name_predicate,
|
|
const 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 = android::base::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(const 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_
|