GNU x86-64汇编简单介绍
GNUx86-64汇编
寄存器
X86-64大约有16个64位整数寄存器,其中栈指针rsp
和基址指针rbp
较为特殊,rsi
和rdi
跟处理字符串相关。后面的八个寄存器是编号的,使用起来没有特殊限制。
- rax rbx rcx rdx
- rsi rdi rbp rsp
- r8 - r15
其中rax的结构如下
[image:ECA803C6-AECB-4593-8CAA-34CF915FBC41-86030-0001242002E5C3CA/20171008192750274.png]
rax
的低八位为al
,接着八位是ah
,合并为ax
,低32位为eax
,整个64位是rax
。
R8的结构如下
[image:8D73982F-FE1D-4750-ADD6-9CDBEC733828-86030-0001243A3CCBAFA8/20171008193142962.png]
大多数编译器产品会混合使用32位和64位模式。32位用来做整数计算,64位一般用来保存内存地址(指针)。
寻址模式
mov
指令有一个决定移动多大数据的单字母前缀
- movb Byte 8bits
- movw Word 16bits
- movl Long 32bits
- movq Quadword 64bits
直接寻址
不同的数据有不同的寻址模式
全局值和函数:直接使用名字,如printf
常数:带有美元符号的立即数,如$56
寄存器:使用寄存器名称,如%rbx
间接寻址
简介寻址是使用与寄存器保存的地址对应的内存中的值
,如(%rsp)表示rsp寄存器指向的内存中的值。
相对基址寻址
表示把一个常数加到寄存器值上,例如-16(%rcx)表示把rcx指向的地址前移16个字节后对应的内存值。
寻址模式相对于管理栈空间、局部变量、函数参数很重要,相对基址寻址也有很多变种,例如-16(%rbx, %rcx, 8)表示-16+%rbx+%rcx*8对应的地址的内存值,这种寻址模式在访问元素大小特殊
的数组时很有用。
下面都表示将一个值加载到rax寄存器上
[image:676C703E-273B-45BF-8548-7A17272C9F8E-86030-000124D32C54E673/20171008200403991.png]
计算
编译器会用到四个基本算数计算指令
- ADD
- SUB
- IDIV
- IMUL
上面的三个操作都有两个操作数,目的操作数在操作以后会被改写。
ADDQ %rbx, %rax
表示rbx的值加上rax的值,写到rax内。在例如写b=b*(b+a)的时候需要注意不要把b的值覆盖了,如下
movq a, %rax
movq b, %rbx
addq %rbx, %rax
imulq %rbx
movq %rax, c
IMUL操作只有一个操作数,表示把%rax
的值乘以操作数,把低64位放在%rax
,高64位放在%rdx
。IDIV相反,把低64位的%rax
,高64位的%rdx
表示的数除以操作数,商放在%rax,余数在%rdx。cdqo
指令会把%rax
符号扩展到%rdx
。
movq a, %rax
cdqo
idivq $5 # divide %rdx:%rax by 5, leaving result in %eax
INC和DEC会把寄存器的值破坏掉。例如,语句a=++b
可以这样翻译:
movq b, %rax
incq %rax
movq %rax, a
布尔操作的工作方式类似,AND,OR,XOR,NOT也会破坏寄存器的值。
小贴士: 浮点数
我们不讨论浮点数操作细节,只需要知道它们使用一套不同的指令和寄存器。在老式机器上,浮点指令是使用可选的外部8087 FPU处理的,所以被称作X87操作,虽然现在已经集成到了CPU里面。X87 FPU包含
8个排列在栈中的80位寄存器(R0-R7)。做浮点算术前,代码必须先把数据push到FPU栈,然后操作栈顶的数据,并回写到内存。内存中双精度浮点数是以64位的长度存储的。这种架构的一个奇怪的地方是,FPU的精度是80位,比内存中的存储方式精度高。结果,浮点计算的值会改变,取决于数据在内存和寄存器之间移动的具体顺序。
浮点数数学计算比它看上去要难懂,推荐阅读:
1. Intel 手册8-1章节。
2. 计算机科学家必知之浮点数
3. 程序员必知之浮点数
---------------------
作者:阿威_t
来源:CSDN
原文:https://blog.csdn.net/pro_technician/article/details/78173777
比较和跳转
JMP指令可以构造一个无限循环,%eax开始计数
movq $0, %rax
loop:
incq %rax
jump loop
所有的比较都用CMP
指令,指令比较两个不同的寄存器中的值,设置eflag寄存器的比特位,记录下结果,jump指令集会利用eflag寄存器中的结果进行跳转
[image:A28DA285-C819-4485-BBCF-63999A382B76-86030-00012629E8112A97/20171013211645843.png]
下面是一个从0累加到5的循环
movq $0, %rax
loop:
incq %rax
cmp $5, %rax
jle loop
设置y的值,如果x大于0,y=10,否则为20
movq x, %rax
cmpq $0, %rax
jle twenty
ten:
movq $10, %rbx
jmp done
twenty:
movq $20, %rbx
jmp done
done:
movq %rbx, y
注意:上面的ten/twenty/done都是标签,标签在一个汇编文件中私有,对外部不可见,除非有.globl标志。c语言的说法,汇编中没有修饰的标签是static的,.globl修饰的标签是extern的。
堆栈
一般内存有如下结构
|----内存高位----|
|--------------|
|--------------|<-------栈底
|--------------|
|--------------|(栈空间向下增长)
|--------------|
|--------------|<-------栈顶
|--------------|
|--------------|
|--------------|<-------堆顶
|--------------|
|--------------|(堆空间向上增长)
|--------------|
|--------------|<-------堆底
|--------------|
|----内存低位----|
函数调用会将参数压入栈中,等调用完后再恢复栈结构,完成一次调用。
%rsp栈指针,指向栈顶,压栈的操作是将%rsp减去8字节,预留出64位,并把%rax写到%rsp指向的内存空间。
subq $8, %rsp
movq %rax, (%rsp)
等价于
pushq %rax
Pop刚好相反
movq (%rsp), %rax
addq $8, %rsp
等价于
popq %rax
如果想丢弃栈中的值,只需要增加%rsp的值
addq $8, %rsp
函数调用
X86-64的函数堆栈System V ABI
较为复杂,这里只做简单的介绍
- 整形参数(和指针)以此放在%rdi, %rsi, %rdx, %rcx, %8, %9寄存器中
- 浮点参数依次放在%xmm0-%xmm7中
- 寄存器不够用时,参数放在栈中
- 可变参数(printf),寄存器%eax需要记录下有多少个浮点参数的个数
- 被调用的函数可以使用任何寄存器,但必须保证%rbx, %rbp, %rsp和%r12-%15恢复到原来的值
- 返回值放在%eax中
[image:E96C7FBD-E6FF-405C-8372-3C74672A64E3-86030-00012E397428F584/20171015115531621.png]
函数调用前,需要先把参数放到寄存器中,将%r10和%r11的值保存到栈中,之后执行call指令,把IP指针的值保存到栈中,然后跳转执行,从函数恢复后,恢复%r10和%r11的值,并从%eax中获取返回值。
long x=0;
long y=10;
int main()
{
x = printf("value: %d", y);
}
对应的汇编
.data
x:
.quad 0
y:
.quad 10
str:
.string "value: %d"
.text
.globl main
main:
movq $str, %rdi
movq y, %rsi
movq $0, %eax #没有浮点数
pushq %r10
pushq %r11
call printf
popq %r11
popq %r10
movq %rax, x
ret
long square(long x)
{
return x*x;
}
.globl square
square:
movq %rdi, %rax
imulq %rdi, %rax
ret
一个复杂函数的调用都有如下步骤
- 改变栈底值
- 将参数依次压入栈中
- 预留函数调用的local variables的空间
- 保护好原有的寄存器rbx, r12-r15
- 函数调用
- 恢复原有的寄存器
- 恢复栈底
.globl func
func:
pushq %rbp # save the base pointer
movq %rsp, %rbp # set new base pointer
pushq %rdi # save first argument on the stack
pushq %rsi # save second argument on the stack
pushq %rdx # save third argument on the stack
subq $16, %rsp # allocate two more local variables
pushq %rbx # save callee-saved registers
pushq %r12
pushq %r13
pushq %r14
pushq %r15
### body of function goes here ###
popq %r15 # restore callee-saved registers
popq %r14
popq %r13
popq %r12
popq %rbx
movq %rbp, %rsp # reset stack to previous base pointer
popq %rbp # recover previous base pointer
ret # return to the caller
%rbp和%rsp之间的内存缴存stack frame也叫做活动记录。
下面是func内部的栈内存布局。
[image:D59227A1-B882-4E26-B2E1-1926878C9833-86030-00012F66B9789F54/20171015133500035.png]
%rbp指明了栈帧的开始。在函数体内,我们可以用%rbp基址相对寻址方式来引用参数和局部变量。参数0在 -8(%rbp)位置,参数1在 -16(%rbp),以此类推。 -32(%rbp) 对应局部变量,-48(%rbp)对应保存的寄存器。%rsp指向栈中最后一个元素。如果栈还要另作他用,则需要向更低地址的区域压栈。(注意:我们假设所有参数和变量都是8字节长度, 实际上不同的类型的长度不一样,对应的偏移也不一样)。
下面是一个真实的汇编
#include <stdio.h>
int sum(int a, int b)
{
return a+b;
}
int main()
{
int x=10;
int y=20;
printf("sum is:%d\n", sum(x,y));
return 0;
}
.globl __Z3sumii ## -- Begin function _Z3sumii
__Z3sumii: ## @_Z3sumii
.cfi_startproc
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl -8(%rbp), %esi
movl %esi, %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.globl _main ## -- Begin function main
_main: ## @main
.cfi_startproc
pushq %rbp #保存栈底
movq %rsp, %rbp #将栈顶用作新的栈底,保存旧栈帧
subq $16, %rsp #预留4个字节作为栈大小
movl $0, -4(%rbp) #0压栈,我也不知道为什么
movl $10, -8(%rbp)#两个变量压栈
movl $20, -12(%rbp)
movl -8(%rbp), %edi#将值写入edi/esi寄存器,准备调用
movl -12(%rbp), %esi
callq __Z3sumii
leaq L_.str(%rip), %rdi
movl %eax, %esi#记录返回值到esi
movb $0, %al
callq _printf
xorl %esi, %esi
movl %eax, -16(%rbp)#保存结果 ## 4-byte Spill
movl %esi, %eax
addq $16, %rsp #恢复栈顶
popq %rbp #恢复栈底
retq
.cfi_endproc
## -- End function
L_.str: ## @.str
.asciz "sum is:%d\n"