x86_64 (Intel 64, AMD64) is an extension to the x86 (IA-32) architecture. In Corepy, x86_64 and x86 are treated as two completely separate CPU architectures. However, for most documentation/discussion purposes in CorePy, the two are interchangeable. With exceptions, x86_64 is a superset of x86. Due to the extended register file, x86_64 is the recommended architecture of the two, even if only developing 32-bit code. The x86 architecture is maintained for backwards compatibility to CPUs lacking the x86_64 extensions. Hereafter, x86 is used as a shorthand for both the x86 and x86_64 architectures unless otherwise specified.
See the Links to Processor ISA References for pointers to architecture manuals.
Contents |
Below is an example of a simple CorePy/x86_64 program in its entirety:
import corepy.arch.x86_64.platform as env import corepy.arch.x86_64.isa as x86 from corepy.arch.x86_64.types.registers import * from corepy.arch.x86_64.lib.memory import MemRef code = env.InstructionStream() proc = env.Processor() params = env.ExecParams() params.p1 = 3 x86.set_active_code(code) lbl_skip = code.get_label("SKIP") x86.xor(rax, rax) x86.cmp(rax, 1) x86.je(lbl_skip) x86.add(rax, MemRef(rbp, 16, data_size = 64)) code.add(lbl_skip) x86.shl(rax, 1) ret = proc.execute(code, mode = 'int', params = params) assert(ret == 6)
This example touches on a number of different aspects/features of CorePy and x86, including labels, parameters, memory references, and x86-specific register usage.
Except for x86, registers in CorePy are managed through an acquire/release mechanism in the InstructionStream class. This functionality exists for x86 as well; however using the registers directly by name is the common practice on x86. The named registers are available in the corepy.arch.{x86,x86_64}.types.registers module and are usually imported directly into the program's namespace (as is done in the example above). It is also common to 'rename' the registers to more human-readable names (one of the many benefits of CorePy!), like the following:
r_count = rcx r_sum = rax x86.mov(r_count, 10) x86.xor(r_sum, r_sum) lbl_loop = code.get_label("LOOP") code.add(lbl_loop) x86.add(r_sum, r_count) x86.dec(r_count) x86.jnz(lbl_loop)
Memory accesses on x86 are far more common than on load/store RISC architectures -- most instructions allow for either a source or destination memory operand. Also some fairly complex addressing is possible on x86. CorePy supports this via the MemoryReference (or the equivalent MemRef shorthand) class. Any of the addressing modes discussed below are available whenever an instruction allows for a memory operand.
Literal 32-bit addresses (even on x86_64) are supported like the following:
x86.mov(rax, MemRef(0xDEADBEEF)) x86.add(MemRef(0xCAFEBABE), rax))
Probably the most common addressing mode is the SIB, or Scale-Index-Base mode. A base register is always required. An index register is optional, and is multiplied by a scale value of 1 (default), 2, 4, or 8. In addition, a displacement (signed 32-bit value) may be specified, defaulting to 0. The formula for computing the effective address (EA) is Base + (Index * Scale) + Displacement. Consider the following examples:
x86.div(MemRef(rbp, 16)) # EA = rbp + 16 x86.and_(rdx, MemRef(rdi, index = r12)) # EA = rdi + r12 x86.shl(MemRef(rsi, 0, rcx, 4, data_size = 32), 1) # EA = rsi + (rcx * 4), 32 bits operand size.
The first argument to MemRef is always the base register. Again, all other arguments are optional. If keyword arguments are not used, this is followed by the displacement, index register, and scale. The index and scale may be specified using keyword arguments, as is done in the AND instruction above. The shift-left instruction above specifies all components of the SIB addressing mode.
Note the use of the data_size keyword argument, which may be used with any addressing mode. Memory operands default to 64-bit on x86_64, and 32-bit on x86. Whenever a non-default operand size is desired (128-bit SSE operands or 32-bit code on x86_64, for example), use the data_size keyword argument to specify the size in bits.
x86_64 supports another addressing mode, called RIP-relative. RIP is the instruction pointer register, and contains the address of the instruction following the one currently being executed. An effective address is formed by adding a 32-bit signed displacement to the value in the RIP register; full SIB-style addresses are not supported. An example:
x86.mov(r15, MemRef(rip, -1024))
Although the x86_64 ABI specifies that the first six parameters are passed via register, CorePy passes parameters via the stack. Currently only a maximum of 8 64-bit integer values may be passed in as parameters. In python, an ExecParams object is used to set the parameters to pass to the synthesized code. ExecParams contains 8 member variables, p1 through p8. Synthesized code accesses these parameters in memory starting with p1 at an EA of rbp + 16, and p8 at an EA of rbp + 72. Although CorePy initializes rbp as a frame pointer in the prologue, it may be desireable to use rbp for other purposes. In this case, p1 is also initially located at an EA of rsp + 72; the displacement will change as the stack pointer is adjusted by pushes and pops.
The example program above passes a single parameter from python and uses a mov instruction to read the value in the synthesized code. An abridged example using all 8 parameters:
params = env.ExecParams() params.p1 = 1 params.p2 = 2 params.p3 = 3 params.p4 = 4 params.p5 = 5 params.p6 = 6 params.p7 = 7 params.p8 = 8 x86.mov(rax, MemRef(rbp, 16)) x86.add(rax, MemRef(rbp, 24)) x86.add(rax, MemRef(rbp, 32)) x86.add(rax, MemRef(rbp, 40)) x86.add(rax, MemRef(rbp, 48)) x86.add(rax, MemRef(rbp, 56)) x86.add(rax, MemRef(rbp, 64)) x86.add(rax, MemRef(rbp, 72))