【编译和链接四】编译器后端——生成汇编代码
- 一、AT&T 汇编语法
- 1、Intel 汇编
- 2、AT&T汇编
- 二、x86寄存器
- 1、x86通用寄存器
- 2.其他寄存器
- 3、寄存器的具体用途
- 三、常见的x86指令
- 四、栈和栈帧
一、AT&T 汇编语法
AT&T VS Intel
基于 x86 架构 的处理器所使用的汇编指令一般有两种格式.
1、Intel 汇编
DOS(8086处理器), Windows
Windows 派系 -> VC 编译器
2、AT&T汇编
Linux, Unix, Mac OS, iOS(模拟器)
Unix派系 -> GCC编译器
汇编语言知多少(四): AT&T 汇编语法
二、x86寄存器
-
寄存器非常小,可以快速访问位于CPU的存储器。某些寄存器有特殊用途,如跟踪当前执行地址的指令指针(EIP/RIP)或跟踪栈顶的栈指针(ESP/RSP)。其他的寄存器主要是通用存储单元,用来存储CPU执行程序时用到的变量。
-
x86基于原始的8086指令集,寄存器是16位宽,而32位x86 ISA将这些寄存器扩展为32位,然后x86-64再将它们进一步扩展为64位。为了保证向后兼容,新指令集中使用的寄存器是旧寄存器的超集。
在汇编中指定寄存器操作数,需要使用寄存器的名称,如mov rax,64将64赋给rax寄存器。图A-2显示了如何将x86-64 rax寄存器细分为传统的32位和16位寄存器,rax的低32位形成一个名为eax的寄存器,其低16位形成一个原始的8086寄存器ax。读者可以通过寄存器名称al访问ax的低字节,通过ah访问ax的高字节。
1、x86通用寄存器
栈指针(rsp)和基址指针(rbp)被认为是特殊寄存器,因为它们用来跟踪栈的布局,当然原则上你也可以将它们作为通用寄存器。
2.其他寄存器
除表A-2中所示的寄存器外,x86 CPU还包含其他非通用寄存器,最重要的两个是rip(在32位x86上称为eip,在8086上称为ip)和rflag(在较早的ISA中称为eflag或者标志寄存器)。rip指令指针始终指向下一条指令的地址,并且由CPU自动设置,无法手动干预。
3、寄存器的具体用途
%rax 作为函数返回值使用.
%rsp 指向栈顶.
%rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10等寄存器用于存放函数参数.
- 内存操作数
内存操作数指的是一个内存地址,CPU 在这个地址获取单个或多字节。x86 ISA对每条指令只支持一个显式内存操作数,也就是说你不能在一条指令中直接将一个值从一个内存地址移动到另一个内存地址,你必须使用寄存器作为中间存储。
在x86中,可以用[base+index*scale+displacement]指定内存操作数,其中base和index是64位寄存器,scale(比例)是1、2、4或8的整数值,而displacement(偏移)是32位常量或符号,所有这些组件都是可选的。CPU计算内存操作数表达式的结果,得到最终的内存地址,base、index和scale均在指令SIB字节中得到表达,而displacement在同名数据域得到表达。scale默认为1,displacement默认为0。
这些内存操作数格式足够灵活,可以直接使用许多常见的代码范例,如可以使用mov eax,DWORD PTR [rax*4+arr]之类的指令访问数组元素,其中arr是数组起始地址的偏移,rax是访问的数组元素的索引值,每个数组元素长度为4字节,DWORD PTR告诉汇编程序要从内存中获取4字节(双字或DWORD)。同样,访问结构域的一种方法是将结构的起始地址存储在基址寄存器,并添加要访问的域的偏移。
- 立即数
立即数就是指令中硬编码的常量整数操作数,如指令add rax,42,42就是一个立即数。
在x86上,立即数以小端格式编码,多字节整数的最低有效字节排在内存中的第一位。换句话说,如果编写像mov ecx,0x10203040这样的程序集指令,相应的机器指令会将指令编码为0x40302010。
参考x86汇编快速入门
三、常见的x86指令
表A-3描述了常见的x86指令。要了解更多未在此表列出的指令,请参考Intel手册或者相应网站。表A-3中列出的大多数指令都是不言自明的,但有些需要更详细的描述。
表A-3 常见的x86指令
- 数据传输
指令 | 描述 |
---|---|
❶ mov dst,src | 将src赋给dst |
xchg dst1,dst2 | 互换dst1和dst2 |
❷ push src | 将src压栈,并递减rsp |
pop dst | 出栈赋给dst,并递增rsp |
- 算术
指令 | 描述 |
---|---|
add dst, src | dst +=src |
sub dst, src | dst –= src |
inc dst | dst += 1 |
dec dst | dst –= 1 |
neg dst | dst = –dst |
❸ cmp src1, src2 | 根据src1−src2设置状态标志位 |
- 逻辑/按位
指令 | 描述 |
---|---|
and dst, src | dst &= src |
or dst, src | dst |
xor dst, src | dst ˆ= src |
not dst | dst = ~dst |
❹ test src1, src2 | 根据src1 & src2设置状态标志位 |
- 无条件分支
指令 | 描述 |
---|---|
jmp addr | 跳转到地址 |
call addr | 压入返回地址到栈上,然后调用函数地址 |
ret | 从栈上弹出返回地址,然后跳转到该地址 |
❺ syscall | 进入内核执行系统调用 |
- 跳转分支(基于状态标志位)jcc addr仅在条件cc成立时才跳转到该地址,否则进入jncc相反条件,在条件cc不成立时跳转
指令 | 描述 |
---|---|
❻ je addr / jz addr | 如果设置ZF零标志位则跳转(如当上一个cmp中的操作数相同时) |
ja addr | 上一次比较中,如果dst大于src则跳转(无符号) |
jb addr | 上一次比较中,如果dst小于src则跳转(无符号) |
jg addr | 上一次比较中,如果dst大于src则跳转(有符号) |
jl addr | 上一次比较中,如果dst小于src则跳转(有符号) |
jge addr | 上一次比较中,如果dst大于等于src则跳转(有符号) |
jle addr | 上一次比较中,如果dst小于等于src则跳转(有符号) |
js addr | 上一次比较中,如果结果为负则跳转,符号位置1 |
- 杂项
指令 | 描述 |
---|---|
❼ lea dst, src | 将内存地址加载到dst中,(dst=&src,其中src必须在内存) |
nop | 空指令,不执行操作(用作代码填充) |
- mov
MOV指令是数据传送指令,也是最基本的编程指令,用于将一个数据从源地址传送到目标地址(寄存器间的数据传送本质上也是一样的)。其特点是不破坏源地址单元的内容。
MOV AX,2000H;将16位数据2000H传送到AX寄存器
MOV AL,20H;将8位数据20H传送到AL寄存器
MOV AX,BX;将BX寄存器的16位数据传送到AX寄存器
MOV AL,[2000H];将2000H单元的内容传送到AL寄存器
# 调整rbp寄存器,使其指向main函数栈帧的起始位置
mov %rsp,%rbp
- push
push %rbp # 保存调用者的rbp寄存器的值
- ret
相当于pop IP
详见汇编常见指令
四、栈和栈帧
参考栈和栈帧
堆栈(stack)又称为栈或堆叠,是计算机科学里最重要且最基础的数据结构之一,它按照FILO(First In Last Out,后进先出)的原则存储数据。
- 栈的相关概念:
- 栈顶和栈底:允许元素插入与删除的一端称为栈顶,另一端称为栈底。
- 压栈:栈的插入操作,叫做进栈,也称压栈、入栈。
- 弹栈:栈的删除操作,也叫做出栈。
下面是栈的示意图,从图中可以清楚的看到,不管是插入数据还是删除数据,都是在栈顶进行的,还有就是FILO原则,可以看到,如果你想取出B的值,那么你必须先要将B的上面的C取出,要取出C的值,就得取出C上面的值,以此类推。
从技术上说,栈就是CPU寄存器里的某个指针所指向的一片内存区域。这里所说的“某个指针”通常位于x86/x64平台的ESP寄存器/RSP寄存器,以及ARM平台的SP寄存器。
操作栈的最常见的指令时PUSH(压栈)和POP(弹栈)。PUSH指令会对ESP/RSP/SP寄存器的值进行减法运算,使之减去4(32位)或8(64位),然后将操作数写到上述寄存器里的指针所指向的内存中。
POP指令是PUSH指令的逆操作:它先从栈指针指向的内存中读取数据,用以备用(通常是写到其他寄存器里),然后再将栈指针的数值加上4或8.
下图演示了x86平台下的push指令和pop指令,指令push Z,首先ESP的值-4,然后将Z的值写入新的ESP所指的内存中,指令pop eax,先将Z的值存入EAX寄存器,然后进行ESP+4。指令POP EBX,首先将栈顶元素存入EBX,然后ESP+4。
栈在进程中的作用如下:
暂时保存函数内的局部变量。
调用函数时传递参数。
保存函数返回的地址。
栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。简言之,栈帧就是利用EBP(栈帧指针,请注意不是ESP)寄存器访问局部变量、参数、函数返回地址等的手段。
;栈帧结构
PUSH EBP ;函数开始(使用EBP前先把已有值保存到栈中)
MOV EBP, ESP ;保存当前ESP到EBP中
... ;函数体
;无论ESP值如何变化,EBP都保持不变,可以安全访问函数的局部变量、参数
MOV ESP, EBP ;将函数的起始地址返回到ESP中
POP EBP ;函数返回前弹出保存在栈中的值
RETN ;函数终止
每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)。每个独立的栈帧一般包括:
- 函数的返回地址和参数
- 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量函数调用的上下文
- 栈是从高地址向低地址延伸,一个函数的栈帧用EBP和ESP这两个寄存器来划定范围。EBP指向当前栈帧的底部,ESP始终指向栈帧的顶部。
EBP寄存器又被称为帧指针(Frame Pointer)
ESP寄存器又被称为栈指针(Stack Pointer)
一个很常见的活动记录示例如图所示