/* +----------------------------------------------------------------------+ | HipHop for PHP | +----------------------------------------------------------------------+ | Copyright (c) 2010-2013 Facebook, Inc. (http://www.facebook.com) | +----------------------------------------------------------------------+ | This source file is subject to version 3.01 of the PHP license, | | that is bundled with this package in the file LICENSE, and is | | available through the world-wide-web at the following url: | | http://www.php.net/license/3_01.txt | | If you did not receive a copy of the PHP license and are unable to | | obtain it through the world-wide-web, please send a note to | | license@php.net so we can mail you a copy immediately. | +----------------------------------------------------------------------+ */ #include "hphp/util/asm-x64.h" #include "gtest/gtest.h" #include #include #include #include #include #include #include #include #include namespace HPHP { namespace Transl { typedef X64Assembler Asm; using namespace reg; namespace { ////////////////////////////////////////////////////////////////////// /* * The following environment variables can be used to turn up the number of * combinations. Off by default to keep the test running fast normally ... */ const bool testMore = getenv("ASM_TEST_MORE"); const bool testMax = getenv("ASM_TEST_MAX"); bool match_opcode_line(const std::string& line, std::string& opName, std::string& opArgs) { static boost::regex re { R"([^\t]*\t[^\t]*\t([a-zA-Z]+)\s+(.*))" }; boost::smatch cm; if (!regex_match(line, cm, re)) return false; opName = cm[1]; opArgs = cm[2]; return true; } void compare(const char* expectedOpName, std::istream& in, const std::vector& expecteds) { auto expectIt = expecteds.begin(); std::string expect, real; std::string opName, opArgs; while (std::getline(in, real)) { if (!match_opcode_line(real, opName, opArgs)) continue; EXPECT_EQ(expectedOpName, opName); if (expectIt == expecteds.end()) { EXPECT_EQ(1, 0) << "Incorrect number of assembler lines"; break; } EXPECT_EQ(*expectIt, opArgs) << "in opcode: " << expectedOpName; ++expectIt; } EXPECT_EQ(expectIt, expecteds.end()) << "More lines expected than read"; } void dump_disasm(Asm& a, std::ifstream& dump) { char filename[32]; char outfile[32]; std::strcpy(filename, "/tmp/asmtmXXXXXX"); std::strcpy(outfile, "/tmp/asmtmXXXXXX"); close(mkstemp(filename)); close(mkstemp(outfile)); std::FILE* fp = std::fopen(filename, "w"); std::fwrite(a.base(), a.used(), 1, fp); std::fclose(fp); system(str(boost::format( "objdump -D -b binary -mi386 -M x86-64 %s > %s") % filename % outfile ).c_str()); dump.open(outfile); unlink(outfile); if (!dump.is_open()) std::abort(); } void expect_asm(Asm& a, const std::string& str) { std::ifstream dump; dump_disasm(a, dump); std::ostringstream out; out << '\n'; std::string line; std::string opName, opArgs; while (std::getline(dump, line)) { if (match_opcode_line(line, opName, opArgs)) { out << opName << ' ' << opArgs << '\n'; } } EXPECT_EQ(out.str(), str); } // Generate a bunch of operands of a given type. template struct Gen; template<> struct Gen { static const std::vector& gen() { if (testMax) { static const std::vector v = { rax, rbx, rcx, rdx, rsp, rbp, rsi, rdi, r8, r9, r10, r11, r12, r13, r14, r15 }; return v; } if (testMore) { static const std::vector v = { rax, rbx, rsp, rbp, r12, r13, r15 }; return v; } static const std::vector v = { rax, rbp, r15 }; return v; } }; template<> struct Gen { static const std::vector& gen() { if (testMax) { static const std::vector v = { eax, ecx, edx, ebx, esp, ebp, esi, edi, r8d, r9d, r10d, r11d, r12d, r13d, r14d, r15d }; return v; } if (testMore) { static const std::vector v = { eax, ebx, esp, ebp, r12d, r13d, r14d }; return v; } static const std::vector v = { eax, ebp, r15d }; return v; } }; template<> struct Gen { static const std::vector& gen() { // We can't really test the high-byte regs here because they can't be // used any time we have a REX byte. if (testMax) { static const std::vector v = { al, cl, dl, bl, spl, bpl, sil, dil, r8b, r9b, r10b, r11b, r12b, r13b, r14b, r15b }; return v; } static const std::vector v = { al, r8b, sil }; return v; } }; template<> struct Gen { static const std::vector& gen() { static bool inited = false; static std::vector vec; if (inited) return vec; auto& regs = Gen::gen(); const std::vector disps = { -1024, 0, 12, 1024 }; for (auto& r : regs) { for (auto& d : disps) { vec.push_back(r[d]); } } inited = true; return vec; } }; template<> struct Gen { static const std::vector& gen() { static bool inited = false; static std::vector vec; if (inited) return vec; auto& indexes = Gen::gen(); auto& mrs = Gen::gen(); std::vector scales = { 4 }; for (auto& mr : mrs) { for (auto& idx : indexes) { if (idx == rsp) continue; for (auto& s : scales) { vec.push_back(*(IndexedDispReg(mr.r.base, idx * s) + mr.r.disp)); } } } return vec; } }; static bool doingByteOpcodes = false; template<> struct Gen { static const std::vector& gen() { if (doingByteOpcodes) { // Don't use any immediates that don't fit in a byte. (Normally we // want to though because they can be encoded different.) static const std::vector vec { 1, 127 }; return vec; } static const std::vector vec { 1, 1 << 20 }; return vec; } }; const char* expected_str(Reg64 r) { return regname(r); } const char* expected_str(Reg32 r) { return regname(r); } const char* expected_str(Reg8 r) { return regname(r); } #undef X void expected_disp_str(intptr_t disp, std::ostream& out) { out << "0x" << std::hex << (uintptr_t)disp; } std::string expected_str(MemoryRef mr) { std::ostringstream out; /* * Operations with "rbp-like" registers still are encoded with a * displacement byte, even if the displacement is zero. This wouldn't * matter, but objdump displays the zero displacements for these * registers (presumably because of this), so we have to add it to our * expected string in that case. */ if (mr.r.base == rbp || mr.r.base == r13 || mr.r.disp != 0) { expected_disp_str(mr.r.disp, out); } out << '(' << expected_str(mr.r.base) << ')'; return out.str(); } std::string expected_str(IndexedMemoryRef imr) { std::ostringstream out; // See above about the rbp/r13 thing. if (imr.r.base == rbp || imr.r.base == r13 || imr.r.disp != 0) { expected_disp_str(imr.r.disp, out); } out << '(' << expected_str(imr.r.base) << ',' << expected_str(imr.r.index) << ',' << imr.r.scale << ')'; return out.str(); } std::string expected_str(Immed i) { std::ostringstream out; out << "$0x" << std::hex << i.q(); return out.str(); } ////////////////////////////////////////////////////////////////////// template void dotest(const char* opName, Asm& a, void (Asm::*memFn)(Arg)) { std::vector expecteds; auto& args = Gen::gen(); for (auto& ar : args) { expecteds.push_back(expected_str(ar)); (a.*memFn)(ar); } std::ifstream dump; dump_disasm(a, dump); compare(opName, dump, expecteds); a.clear(); } template void dotest(const char* opName, Asm& a, void (Asm::*memFn)(Arg1, Arg2), const std::vector& args1, const std::vector& args2) { std::vector expecteds; for (auto& ar1 : args1) { for (auto& ar2 : args2) { expecteds.push_back(str( boost::format("%s,%s") % expected_str(ar1) % expected_str(ar2) )); (a.*memFn)(ar1, ar2); } } std::ifstream dump; dump_disasm(a, dump); compare(opName, dump, expecteds); a.clear(); } template void dotest(const char* opName, Asm& a, void (Asm::*memFn)(Arg1, Arg2)) { dotest(opName, a, memFn, Gen::gen(), Gen::gen()); } ////////////////////////////////////////////////////////////////////// // Wrappers for generating test cases for various addressing modes. typedef void (Asm::*OpR64)(Reg64); typedef void (Asm::*OpR32)(Reg32); typedef void (Asm::*OpR8)(Reg8); typedef void (Asm::*OpRR64)(Reg64, Reg64); typedef void (Asm::*OpRR32)(Reg32, Reg32); typedef void (Asm::*OpRR8)(Reg8, Reg8); typedef void (Asm::*OpR8R32)(Reg8, Reg32); typedef void (Asm::*OpR8R64)(Reg8, Reg64); typedef void (Asm::*OpMR64)(MemoryRef, Reg64); typedef void (Asm::*OpMR32)(MemoryRef, Reg32); typedef void (Asm::*OpMR8)(MemoryRef, Reg8); typedef void (Asm::*OpSMR64)(IndexedMemoryRef, Reg64); typedef void (Asm::*OpSMR32)(IndexedMemoryRef, Reg32); typedef void (Asm::*OpSMR8)(IndexedMemoryRef, Reg8); typedef void (Asm::*OpRM64)(Reg64, MemoryRef); typedef void (Asm::*OpRM32)(Reg32, MemoryRef); typedef void (Asm::*OpRM8)(Reg8, MemoryRef); typedef void (Asm::*OpRSM64)(Reg64, IndexedMemoryRef); typedef void (Asm::*OpRSM32)(Reg32, IndexedMemoryRef); typedef void (Asm::*OpRSM8)(Reg8, IndexedMemoryRef); typedef void (Asm::*OpIR64)(Immed, Reg64); typedef void (Asm::*OpIR32)(Immed, Reg32); typedef void (Asm::*OpIR8)(Immed, Reg8); typedef void (Asm::*OpIM64)(Immed, MemoryRef); typedef void (Asm::*OpIM32)(Immed, MemoryRef); typedef void (Asm::*OpIM16)(Immed, MemoryRef); typedef void (Asm::*OpISM64)(Immed, IndexedMemoryRef); typedef void (Asm::*OpISM32)(Immed, IndexedMemoryRef); typedef void (Asm::*OpISM16)(Immed, IndexedMemoryRef); ////////////////////////////////////////////////////////////////////// } TEST(Asm, General) { Asm a; a.init(10 << 24); /* * Test is a little different, so we have this BASIC_OP stuff. * * Skips using a memory source operand---there's actually only a test * instruction with memory destination (even though our API allows * writing it the other way), so when we disassemble the args look like * the wrong order. */ #define BASIC_OP(op) \ dotest(#op, a, OpRR32(&Asm::op##l)); \ dotest(#op, a, OpRR64(&Asm::op##q)); \ dotest(#op, a, OpRM32(&Asm::op##l)); \ dotest(#op, a, OpRM64(&Asm::op##q)); \ dotest(#op, a, OpRSM32(&Asm::op##l)); \ dotest(#op, a, OpRSM64(&Asm::op##q)); \ dotest(#op, a, OpIR64(&Asm::op##q)); \ dotest(#op, a, OpIR32(&Asm::op##l)); \ dotest(#op "q", a, OpIM64(&Asm::op##q)); \ dotest(#op "l", a, OpIM32(&Asm::op##l)); \ dotest(#op "q", a, OpISM64(&Asm::op##q)); \ dotest(#op "l", a, OpISM32(&Asm::op##l)); #define FULL_OP(op) \ BASIC_OP(op) \ dotest(#op, a, OpMR32(&Asm::op##l)); \ dotest(#op, a, OpMR64(&Asm::op##q)); \ dotest(#op, a, OpSMR32(&Asm::op##l)); \ dotest(#op, a, OpSMR64(&Asm::op##q)); #define BASIC_BYTE_OP(op) \ dotest(#op, a, OpIR8(&Asm::op##b)); \ dotest(#op, a, OpRR8(&Asm::op##b)); \ dotest(#op, a, OpRM8(&Asm::op##b)); \ dotest(#op, a, OpRSM8(&Asm::op##b)); \ dotest(#op, a, OpIR8(&Asm::op##b)); #define FULL_BYTE_OP(op) \ BASIC_BYTE_OP(op) \ dotest(#op, a, OpMR8(&Asm::op##b)); \ dotest(#op, a, OpSMR8(&Asm::op##b)); #define UNARY_BYTE_OP(op) \ dotest(#op, a, OpR8(&Asm::op##b)); dotest("inc", a, OpR32(&Asm::incl)); dotest("inc", a, OpR64(&Asm::incq)); dotest("mov", a, OpRR8(&Asm::movb)); dotest("mov", a, OpRR32(&Asm::movl)); dotest("mov", a, OpRR64(&Asm::movq)); dotest("mov", a, OpMR8(&Asm::loadb)); dotest("mov", a, OpMR32(&Asm::loadl)); dotest("mov", a, OpMR64(&Asm::loadq)); dotest("mov", a, OpSMR8(&Asm::loadb)); dotest("mov", a, OpSMR32(&Asm::loadl)); dotest("mov", a, OpSMR64(&Asm::loadq)); dotest("mov", a, OpRM8(&Asm::storeb)); dotest("mov", a, OpRM32(&Asm::storel)); dotest("mov", a, OpRM64(&Asm::storeq)); dotest("movl",a, OpISM32(&Asm::storel)); dotest("movq",a, OpISM64(&Asm::storeq)); dotest("mov", a, OpRSM8(&Asm::storeb)); dotest("mov", a, OpRSM32(&Asm::storel)); dotest("mov", a, OpRSM64(&Asm::storeq)); dotest("movzbl", a, OpMR32(&Asm::loadzbl)); dotest("movzbl", a, OpSMR32(&Asm::loadzbl)); dotest("movzbl", a, OpR8R32(&Asm::movzbl)); dotest("movsbq", a, OpMR64(&Asm::loadsbq)); dotest("movsbq", a, OpSMR64(&Asm::loadsbq)); dotest("movsbq", a, OpR8R64(&Asm::movsbq)); FULL_OP(add); FULL_OP(xor); FULL_OP(sub); FULL_OP(and); FULL_OP(or); FULL_OP(cmp); BASIC_OP(test); // Note: objdump disassembles xchg %rax,%rax as rex.W nop, so we're just // leaving it out. doingByteOpcodes = true; FULL_BYTE_OP(cmp); BASIC_BYTE_OP(test); UNARY_BYTE_OP(not); UNARY_BYTE_OP(neg); doingByteOpcodes = false; } TEST(Asm, WordSizeInstructions) { Asm a; a.init(10 << 24); // single register operations a. incw (ax); // single memory operations a. decw (*r8); // register-register operations a. addw (ax, bx); a. xorw (r10w, r11w); a. movw (cx, si); // register-memory operations a. storew (ax, *rbx); a. testw (r10w, rsi[0x10]); // memory-register operations a. subw (*rcx, ax); a. orw (r11[0x100], dx); // immediate-register operations a. shlw (0x3, di); a. andw (0x5555, r12w); // immediate-memory operations a. storew (0x1, *r9); a. storew (0x1, rax[0x100]); expect_asm(a, R"( inc %ax decw (%r8) add %ax,%bx xor %r10w,%r11w mov %cx,%si mov %ax,(%rbx) test %r10w,0x10(%rsi) sub (%rcx),%ax or 0x100(%r11),%dx shl $0x3,%di and $0x5555,%r12w movw $0x1,(%r9) movw $0x1,0x100(%rax) )"); } TEST(Asm, RetImmediate) { Asm a; a.init(10 << 24); a.ret(8); ASSERT_FALSE(a.base()[0] == kOpsizePrefix); } TEST(Asm, IncDecRegs) { Asm a; a.init(10 << 24); // incq, incl, incw a. incq(rax); a. incl(eax); a. incw(ax); a. incq(r15); a. incl(r15d); a. incw(r15w); // decq, decl, decw a. decq(rax); a. decl(eax); a. decw(ax); a. decq(r15); a. decl(r15d); a. decw(r15w); expect_asm(a, R"( inc %rax inc %eax inc %ax inc %r15 inc %r15d inc %r15w dec %rax dec %eax dec %ax dec %r15 dec %r15d dec %r15w )"); } TEST(Asm, HighByteReg) { Asm a; a.init(10 << 24); // Test movzbl with high byte regs, avoiding destination registers // that need a rex prefix std::vector hiregs = {ah, bh, ch, dh}; std::vector reg32s = {eax, ecx, esi, ebp}; dotest("movzbl", a, OpR8R32(&Asm::movzbl), hiregs, reg32s); a. movb (al, ah); a. testb (0x1, ah); a. cmpb (ch, dh); expect_asm(a, R"( mov %al,%ah test $0x1,%ah cmp %ch,%dh )"); } TEST(Asm, RandomJunk) { Asm a; a.init(10 << 24); a. push (rbp); a. movq (rsp, rbp); a. subq (0x80, rsp); a. movl (0, eax); a. incq (rax); a. storeq (rax, rsp[0x8]); a. loadq (rsp[0x8], rdi); a. pop (rbp); a. ret (); expect_asm(a, R"( push %rbp mov %rsp,%rbp sub $0x80,%rsp mov $0x0,%eax inc %rax mov %rax,0x8(%rsp) mov 0x8(%rsp),%rdi pop %rbp retq )" "\n"); // string concat to avoid space at end of line after retq } TEST(Asm, AluBytes) { Asm a; a.init(10 << 24); #define INSTRS \ FROB(cmp) \ FROB(add) \ FROB(sub) \ FROB(and) \ FROB(or) \ FROB(xor) #define FROB(instr) \ a. instr ## b(sil, al); \ a. instr ## b(0xf, al); \ a. instr ## b(sil, rcx[0x10]); \ a. instr ## b(rsp[0x10], sil); \ a. instr ## b(rcx[rsi * 8], al); \ a. instr ## b(al, rcx[rsi * 8]); INSTRS #undef FROB #define FROB(name) \ #name " %sil,%al\n" \ #name " $0xf,%al\n" \ #name " %sil,0x10(%rcx)\n" \ #name " 0x10(%rsp),%sil\n" \ #name " (%rcx,%rsi,8),%al\n" \ #name " %al,(%rcx,%rsi,8)\n" expect_asm(a, "\n" INSTRS ""); #undef FROB #undef INSTRS // test is asymmetric. a.clear(); a. testb(sil, al); a. testb(0xf, al); a. testb(sil, rcx[0x10]); a. testb(sil, rcx[rsi * 8]); expect_asm(a, R"( test %sil,%al test $0xf,%al test %sil,0x10(%rcx) test %sil,(%rcx,%rsi,8) )"); } TEST(Asm, CMov) { Asm a; a.init(10 << 24); a. test_reg64_reg64(rax, rax); a. cload_reg64_disp_reg64(CC_Z, rax, 0, rax); a. cload_reg64_disp_reg32(CC_Z, rax, 0, rax); expect_asm(a, R"( test %rax,%rax cmove (%rax),%rax cmove (%rax),%eax )"); } TEST(Asm, SimpleLabelTest) { Asm a; a.init(10 << 24); Label loop; auto loopCallee = [] (int* counter) { ++*counter; }; // Function that calls loopCallee N times. auto function = reinterpret_cast(a.frontier()); a. push (rbp); a. movq (rsp, rbp); a. push (r15); a. push (r12); a. push (r10); a. push (rbx); a. movl (edi, r12d); a. movq (rsi, r15); a. movl (0, ebx); asm_label(a, loop); a. movq (r15, rdi); a. call (CodeAddress(static_cast(loopCallee))); a. incl (ebx); a. cmpl (ebx, r12d); a. jne (loop); a. pop (rbx); a. pop (r10); a. pop (r12); a. pop (r15); a. pop (rbp); a. ret (); auto test_case = [&] (int n) { int counter = 0; function(n, &counter); EXPECT_EQ(n, counter); }; for (int i = 1; i < 15; ++i) test_case(i); test_case(51); test_case(127); } TEST(Asm, ShiftingWithCl) { Asm a; a.init(10 << 24); a. shlq(rax); a. shlq(rdx); a. shlq(r8); a. sarq(rbx); a. sarq(rsi); a. sarq(r8); expect_asm(a, R"( shl %cl,%rax shl %cl,%rdx shl %cl,%r8 sar %cl,%rbx sar %cl,%rsi sar %cl,%r8 )"); } }}