はじめに
x86-64プロセッサでAssembly Codeを作成する方法を説明します。
Assembly CodeをMachine Codeに変換するプログラムをAssemblerといいます。
ここではGNU assembler(GAS)を使用します。
Assembly Codeには決まり事があります。
この決まり事はApplication binary interface(ABI) or Calling conventionと呼ばれており、
instruction set / OS / compilerによって変わります。
x86-64プロセッサの代表的なABIは次の通りです。
それぞれUnix like OS / Windowsに対応します。
- System V AMD64 ABI
- Microsoft x64 calling convention
ここではSystem V AMD64 ABIについて説明します。
cpu / os / gcc
使用したcpu / os / gccを明記します。
- CPU Intel(R) Core(TM) i5-5200U CPU @ 2.20GHz
- OS ubuntu-14.04-desktop-amd64
- gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2
basic information
data type, register, instructionを説明します。
- data type
- general purpose register
- instructions
次の資料を参考にしました。
[1] System V Application Binary Interface AMD64 Architecture Processor Supplement Draft Version 0.3
[2]Computer Systems - A Programmers Perspective
[3]x86-64 Machine-Level Programming Randal E. Bryant David R. O’Hallaron September 9, 2005
[4]Notes on x86-64 programming
data type
x86-64 registers, memory and operationsは次に示すdata typesを使用します。
次の表はC declaration / Intel data type / GAS suffix / x86-64 Sizeの対応表です。
C declaration | Intel data type | GAS suffix | x86-64 Size (Bytes) |
---|---|---|---|
char | Byte | b | 1 |
short | Word | w | 2 |
int | Double word | l | 4 |
unsigned | Double word | l | 4 |
long int | Quad word | q | 8 |
unsigned long | Quad word | q | 8 |
char * | Quad word | q | 8 |
float | Single precision | s | 4 |
double | Double precision | d | 8 |
long double | Extended precision | t | 16 |
Long / Pointerは64bitです。LP64として知られています。
なお、Microsoft x64 calling convention は
Long Long / Pointerが64bitです。LLP64として知られています。
GAS suffixとはGNU assemblerのニーモニックの後ろに付く文字です。
mov命令を例にすると、
movbはオペランドが1byteであることを、
movqはオペランドが8byteであることを示します。
なお、GAS suffixは省略できます。具体的には
movl %eax, %ebx と記載しなくても、単にmov %eax, %ebxでよいです。
general purpose register
16個のgeneral purpose registers(GPRs)が利用できます。サイズは64bit長です。
63 31 15 7 0 bit
-------------------------------------------------------
| rax | eax ax | ah | al |
-------------------------------------------------------
| rbx | ebx bx | bh | bl |
-------------------------------------------------------
| rcx | ecx cx | ch | cl |
-------------------------------------------------------
| rdx | edx dx | dh | dl |
-------------------------------------------------------
| rsi | esi si | | sil |
-------------------------------------------------------
| rdi | edi di | | dil |
-------------------------------------------------------
| rbp | ebp bp | | bpl |
-------------------------------------------------------
| rsp | esp sp | | spl |
-------------------------------------------------------
| r8 | r8d r8w | | r8b |
-------------------------------------------------------
| r9 | r9d r9w | | r9b |
-------------------------------------------------------
| r10 | r10d r10w | | r10b |
-------------------------------------------------------
| r11 | r11d r11w | | r11b |
-------------------------------------------------------
| r12 | r12d r12w | | r12b |
-------------------------------------------------------
| r13 | r13d r13w | | r13b |
-------------------------------------------------------
| r14 | r14d r14w | | r14b |
-------------------------------------------------------
| r15 | r15d r15w | | r15b |
-------------------------------------------------------
同じregisterを32bit / 16bit / 8bit長でアクセスできます。
raxを例に説明します。名称と対応するbitは以下の通りです。
raxは64bit
eaxは下位32bit
axは下位16bit
ahは下位16bitの上位8bit
alは下位16bitの下位8bit
各registerは次に示すように用途が決められています。
- Return value : rax
- Argument : rdi(1st) rsi(2nd) rdx(3rd) rcx(4th) r8(5th) r9(6th)
- Callee saved : rbx rbp r13-15
- Stack pointer : rsp
(*)資料[1]には
r10はtemporary register, used for passing a function’s static chain pointer
r11はtemporary register
r12はcallee-saved register
と記載されています。
Callee saved registerは呼出し先で変更できないregisterです。
呼出し元の視点ではcall命令の前後で値が変わらないことが保証されます。
呼出し先でCallee saved registerを利用する場合は
registerの値をmemoryに保存・復元する必要があります。
次のregisterは関数の引数に利用されます。
Arg # | Size (bits) | > | > | > |
---|---|---|---|---|
64 | 32 | 16 | 8 | |
1 | %rdi | %edi | %di | %dil |
2 | %rsi | %esi | %si | %sil |
3 | %rdx | %edx | %dx | %dl |
4 | %rcx | %ecx | %cx | %cl |
5 | %r8 | %r8d | %r8w | %r8b |
6 | %r9 | %r9d | %r9w | %r9b |
必要に応じてcall命令の前にregisterに値を設定します。
例えばadd(int a, int b)という関数をadd(10, 20)のように呼び出す場合、
次のAssembly Codeになります。%esi(1st)に10を%edi(2nd)に20を設定してcallします。
.global main
format:
.asciz "%d\n"
main:
mov $10, %esi
mov $20, %edi
call add
mov $format, %rdi
mov %eax, %esi
xor %rax, %rax
call printf
ret
add:
xor %rax, %rax
add %edi, %eax
add %esi, %eax
ret
instructions
x86-64 Instruction setを説明します。
以降の説明で用いる用語を説明します。
- Iは直値です。
- Rはregisterです。
- Sは直値 or register or memoryです。
- Dはregister or memoryです。
- Mはmemoryです。
- (*1)のInstructionには後ろに[b|w|l|q]が付きます。
- (*2)のInstructionには後ろに[bw|bl|bq|wl|wq|lq]が付きます。
(*1)の例としてmov(*1)の場合、
movb / movw / movl / movqとなります。b,w,l,qはGAS suffixです。
(*2)の例としてmovs(*2)の場合、
movsbw / movsbl / movsbq / movswl / movswq / movslqとなります。
bwの意味は b → wの変換をするという意味です。bl/bq/wl/wq/lqも同様の意味です。
Data movement
register / memory間でデータをコピーする命令を説明します。
Instructionの一覧を示します。
Instruction | Effect | Description |
---|---|---|
mov(*1) S, D | S → D | Move |
movabsq I, R | I → R | Move quad word |
movs(*2) S, R | SignExtend(S) → R | Move sign-extended |
movz(*2) S, R | ZeroExtend(S) → R | Move zero-extended |
pushq S | R[%rsp]-8 → R[%rsp]; S → M[R[%rsp]] |
Push |
popq D | M[R[%rsp]] → D; R[%rsp]+8 → R[%rsp] |
Pop |
movは同一サイズのデータをコピーする命令です。DにSをコピーします。
movabsqは64bitの直値を64bit registerにコピーします。
movsはsign extension, movzはzero extensionします。詳細は後で説明します。
pushq / popqはstackに8byteデータをpush / popします。
(*)movzlq命令はありません。理由は[3]に次のように記載されています。
Perhaps unexpectedly, instructions that move or generate
32-bit register values also set the upper 32 bits of the register to zero.
Consequently there is no need for an instruction movzlq.
sign extension / zero extension
データをサイズの大きい領域にコピーする場合、次の2つの方式があります。
- sign extension
- zero extension
sign extensionは
- コピー元の最上位ビットが1であればコピー先の上位ビットに1を埋める、
- コピー元の最上位ビットが0であればコピー先の上位ビットに0を埋める、
方式です。
zero extensionはコピー先の上位ビットに0を埋める方式です。
sign extensionでは符号が変化しません。
zero extensionでは符号が変化します。
扱う整数が符号付きかどうかで使い分けます。
例.1byte を 2byteに代入するケース
case 1: 0010 1101 (signed +0x2d, unsigned 0x2d)
0010 1101 b -> 0000 0000 0010 1101 b (sign ext; +0x002d)
0010 1101 b -> 0000 0000 0010 1101 b (zero ext; 0x002d)
case 2: 1010 1101 (signed -0x53, unsigned 0xad)
1010 1101 b -> 1111 1111 1010 1101 b (sign ext; -0x0053)
1010 1101 b -> 0000 0000 1010 1101 b (zero ext; 0x00ad)
Addressing Modes
mov命令はmemoryを参照できます。memory addressは次の形式で参照します。
effective addressといいます。
offset(base,index,scale)
effective addressは(base + index * scale + offset)で計算します。
offsetは固定値 or labelです。base / indexはregisterです。
scaleは1 or 2 or 4 or 8です。
例. %rsp
# %rspに設定されたaddressにあるwordを%diにコピーします。
# offset / index / scaleは省略されています。
mov (%rsp), %di
# mov %rsp, %di
# ()なしにすると%rspの内容をコピーすることになります。誤りです。
PC-relative addressing
x86-64でコンパイルした実行ファイルをobjdumpでdisassembleすると
%ripからの相対アドレスで表現している命令が多くあることに気づきます。
%ripを使ったAddressingをPC-relative addressingといいます。
例.
$ objdump -d test | grep rip
4003e4: 48 8b 05 0d 0c 20 00 mov 0x200c0d(%rip),%rax # 600ff8 <_DYNAMIC+0x1d0>
400400: ff 35 02 0c 20 00 pushq 0x200c02(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
(...)
$
この形式が使われる理由は次の通りです。
Offsets are limited to 32 bits. This means that only a 4GB window into the
potential 64-bit address space can be accessed from a given base value. This is
mainly an issue when accessing static global data. It is standard to access this
data using PC-relative addressing (using %rip as the base). For example,
we would write the address of a global value stored at location labeled a as
a(%rip), meaning that the assembler and linker should cooperate to compute the
offset of a from the ultimate location of the current instruction.
Integer arithmetic & logic operations
Integerのarithmetic & logic operationsを説明します。
- lea
- arithmetic : inc / dec / add / sub
- logic : neg / not / xor / or / and
- shift : sal / sar / shl / shr
次のInstructionはすべて(*1)となります。
Instruction | Effect | Description |
---|---|---|
lea S, D | &S → D | Load effective address |
inc D | D+1 → D | Increment |
dec D | D-1 → D | Decrement |
add S, D | D+S → D | Add |
sub S, D | D-S → D | Subtract |
neg D | -D → D | Negate |
not D | ~D →D | Complement |
xor S, D | D^S → D | Exclusive-or |
or S, D | D|S → D | Or |
and S, D | D&S → D | And |
sal k, D | D<<k → D | Left shift |
shl k, D | D<<k → D | Left shift (same as sal) |
sar k, D | D>>k → D | Arithmetic right shift |
shr k, D | D>>k → D | Logical right shift |
lea / sarについて補足説明します。
lea はeffective addressをDに設定します。
例. lea と movの違い
movは%ripが指し示すaddressの先の8byteを%rsiにコピーします。
leaは%ripが指し示すaddressを%rsiにコピーします。
lea (%rip), %rsi # 0x400542
mov (%rip), %rsi # 0xfffffebfe8c03148
sarは最上位ビットを保持したまま右bit shiftします。
つまり
最上位ビットが1であればシフトしてできる左側には1を詰めます。
最上位ビットが0であればシフトしてできる左側には0を詰めます。
例. sar / shrの違い
mov $0x80, %sil
sar $7, %sil # %sil = 0xFF
mov $0x80, %sil
shr $7, %sil # %sil = 0x01
Multiplication and division operations
Instruction | Effect | Description |
---|---|---|
imulq S | S × R[%rax]→R[%rdx]:R[%rax] | Signed full multiply |
mulq S | S × R[%rax]→R[%rdx]:R[%rax] | Unsigned full multiply |
cltq | SignExtend(R[%eax])→R[%rax] | Convert %eax to quad word |
cqto | SignExtend(R[%rax])→R[%rdx]:R[%rax] | Convert to oct word |
idivq S | R[%rdx]:R[%rax] mod S→R[%rdx] R[%rdx]:R[%rax]÷S→R[%rax] |
Signed divide |
divq S | R[%rdx]:R[%rax] mod S→R[%rdx] R[%rdx]:R[%rax]÷S→R[%rax] |
Unsigned divide |
- sample
.global main
format:
.asciz "%016lx %016lx\n"
main:
movabs $0xFFFFFFFFFFFFFFFF, %rax
movabs $0xFFFFFFFFFFFFFFFF, %r10
mul %r10 # %rdx:%rax
mov $format, %rdi
mov %rdx, %rsi
mov %rax, %rdx
xor %rax, %rax
call printf
ret
FLAGS register
CPUは直前に実行したinstructionに応じて状態をFLAGS registerに保持します。
FLAGS registerを使って処理を分岐させます。
pushf / popf instructionでFLAGS registerをstackにpush / popできます。
pushした値をmovでreadすることで FLAGS registerを読み込めます。
FLAGS registerのbit assignは次の通りです。
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
-------------------------------------------------
| | | | |OF|DF|IF|TF|SF|ZF| 0|AF| 0|PF| 1|CF|
-------------------------------------------------
CF:Carry Flag
PF:Parity Flag
AF:Adjust Flag
ZF:Zero Flag
SF:Sign Flag
TF:Trap Flag
IF:Interrupt Enable Flag
DF:Direction Flag
OF:Overflow Flag
read方法は次の通りです。
.global main
format:
.asciz "0x%04x\n"
main:
xor %rax, %rax
pushf
mov (%rsp), %ax
popf
mov %eax, %esi
mov $format, %rdi
xor %rax, %rax
call printf
ret
FLAGS registerをprintします。
.global main
format:
.asciz "ZF=%01x SF=%01x CF=%01x OF=%01x\n"
print_flags:
xor %r13, %r13
mov %di, %r13w
mov $format, %rdi # 1st
xor %rsi, %rsi # 2nd ZF
bt $6, %r13w
lahf
mov %ah, %al
mov %al, %sil
and $0x01, %sil
xor %rdx, %rdx # 3rd SF
bt $7, %r13w
lahf
mov %ah, %al
mov %al, %dl
and $0x01, %dl
xor %rcx, %rcx # 4th CF
mov %r13b, %cxl
and $0x01, %cxl
xor %r8, %r8 # 5th OF
bt $11, %r13w
lahf
mov %ah, %al
mov %al, %r8b
and $0x01, %r8b
xor %rax, %rax
call printf
ret
main:
xor %rdi, %rdi # %rdi=0
mov $0x7f, %bl
add $0x00, %bl
pushf
mov (%rsp), %di # %di=FLAGS register
popf
call print_flags
ret
ZF:Zero Flag
0であれば1、そうでなければ0になります。
例.
mov $0x00 %bl
test %bl, %bl # ZF = 1
mov $0x01 %bl
test %bl, %bl # ZF = 0
SF:Sign Flag
負であれば1、そうでなければ0になります。
結果の最上位ビットを返します。負であれば最上位ビットは1です。
例.
mov $0x80 %bl
test %bl, %bl # SF = 1
mov $0x7f %bl
test %bl, %bl # SF = 0
mov $0x00 %bl
test %bl, %bl # SF = 0
CF:Carry Flag
符号なし演算としてcarry or a borrowが発生すれば1、そうでなければ0になります。
例. 符号なし 8bit range(0~255)
mov $0xff %bl
add $0x01 %bl # CF = 1, 255+1=256
mov $0xfe %bl
add $0x01 %bl # CF = 0, 254+1=255
mov $0x01 %bl
sub $0x01 %bl # CF = 0, 1-1=0
mov $0x01 %bl
sub $0x02 %bl # CF = 1, 1-2=-1
OF:Overflow Flag
符号付き演算としてcarry or a borrowが発生すれば1、そうでなければ0になります。
例. 符号付き 8bit range(-128~0~127), 0xff(-1)~0x80(-128)~0x7f(127)~0x00(0)
mov $0x7f %bl
add $0x01 %bl # OF = 1, 127+1=128
mov $0x7e %bl
add $0x01 %bl # OF = 0, 126+1=127
mov $0x80 %bl
sub $0x01 %bl # OF = 1, -128-1=-129
mov $0x81 %bl
sub $0x01 %bl # OF = 0, -127-1=-128
Condition Codes
FLAGS registerの組み合わせに応じてCondition Codes(cc)が決まります。
Condition Codesに対応したjump命令があります。
cmp / test命令を利用するとregisterを変更することなしにFLAGS registerを更新できます。
- cmp s2,s1 : set flags based on s1 - s2
- test s2,s1 : set flags based on s1&s2 (logical and)
cmp実行後のFLAGS registerとccの対応を次に示します。
ccに応じてjump命令が決まります。
フォーマットはjccとなります。例. cc=eであればjeとなります。
cc | condition tested | meaning after cmp |
---|---|---|
e | ZF | equal to zero |
ne | ˜ ZF | not equal to zero |
s | SF | negative |
ns | ˜ SF | non-negative |
g | ˜ (SF xor OF) & ˜ ZF | greater (< signed) |
ge | ˜ (SF xor OF) | greater or equal (<= signed) |
l | SF xor OF | less (signed <) |
le | (SF xor OF) | ZF | less or equal (signed <=) |
a | ˜ CF & ˜ ZF | above (< unsigned) |
ae | ˜ CF | above or equal (<= unsigned) |
b | CF | below (< unsigned) |
be | CF | ZF | below or equal (<= unsigned) |
sample code
printf
レジスタの値をprintfするだけのsimpleなAssembly Codeを示します。
esiをprintfします。
.global main
main:
mov $format, %rdi # 1st parameter
mov $10, %rsi # 2nd parameter
xor %rax, %rax #
call printf # printf("%d\n", %rsi)
ret
format:
.asciz "%d\n"
gcc -O2 test.s -o test && ./test
10
rdtscp
rdtscp命令は次の2つの情報を読み出すことができます。
- Time-Stamp Counter
- Processor ID
命令実行後に次のレジスタが更新されます。
- edx:eax : 64bit Time-Stamp Counter
- ecx : Processor ID
Time-Stamp CounterはCPUのclockに合わせてインクリメントされるカウンタ値です。
sample codeを示します。rdtscpを連続して実行します。
1度目のrdtscp後にEAXを退避してすぐに2度目のrdtscpを実行します。
1度目と2度目の差分のカウンタ値をprintfします。
ばらつきがありますが最小で77 clockかかりました。
.global main
main:
movl $10, %r13d
loop:
rdtscp
mov %eax, %r8d
rdtscp
mov %eax, %r9d
sub %r8d, %r9d
mov $format, %rdi # 1st parameter
mov %r9d, %esi # 2nd parameter
mov %ecx, %edx # 3rd parameter
xor %rax, %rax #-
call printf # printf
decl %r13d
jne loop
ret
format:
.asciz "%03d %d\n"
$ ./test
088 1
077 1
088 1
099 1
088 1
...