一、汇编常见指令
1、mov 和 movl
将源操作数的内容复制到目标操作数, 两者必须有一个是寄存器
mov M/R, L/M/R
mov %rax(%rbx) #(%rbx)表示把%rbx的值作为地址,取值。
D(R) 表示根据地址R+D取值注意:寄存器的值是值或者地址
2、xchg
交换两个操作数的内容, 两者必须有一个是寄存器
xchg M/R, M/R
3、add 和 sub
add 为加法指令, sub 为减法指令. 两个操作数必须有一个是寄存器
add M/R, L/M/R
sub M/R, L/M/R
4、neg 切换操作数的正负号, 即求补码
neg M/R
5、mul 和 div
mul 为无符号乘法, div 为无符号除法 mul/div 的积存储在 edx : eax 中, 其中eax作被乘数, 只支持单个操作数
mul M/R
6、SHL 和 SHR
SHL 和 SHR 为逻辑左移 / 逻辑右移. 每移动 n 位二进制数,如果值为负数, 进行逻辑右移后其可能会出错.
shl M/R, L
shr M/R, L
7、SAL 和 SAR
SAL 和 SLR 为算数左移 / 算术右移. 每移动 n 位二进制数,负数进行算术右移不会像逻辑右移一样出现错误.
sal M/R, L
sar M/R, L
8、lea
加载有效地址, 将源操作数的地址加载到目标操作数中. 由于实际地址要在程序运行时才能知道, 因此涉及这些地址的操作应该用 lea 来完成. 该指令的目标操作数必须是寄存器
lea指令可以用来将一个内存地址直接赋给目的操作数
lea R, M
9、push和pop
PUSH 指令首先减少 ESP 的值(栈顶下降),再将源操作数复制到堆栈。操作数是 16 位的,则 ESP 减 2,操作数是 32 位的,则 ESP 减 4。 POP 指令首先把 ESP 指向的堆栈元素内容复制到一个 16 位或 32 位目的操作数中,再增加 ESP 的值。
PUSH reg/mem
pop reg/mem
10、cmp和test
cmp的功能相当于减法指令,只是对操作数之间运算比较,不保存结果。cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过这些标志寄存器位来得知结果。
test将两个操作数进行逻辑与运算,并根据运算结果设置相关的标志位。
11、CS和IP
CS是代码段寄存器(也就是储存指令)、IP是指令指针寄存器。
8086机器中,任意时刻,CPU将CS:IP指向的内容当作指令执行。 意思是,任意时刻,设CS中的内容是M,IP中的内容为N,8086CPU将从内存M*16+N单元开始,读取一条指令并执行。
12、jmp和loop
JMP 指令无条件跳转到目标地址
JMP destination
13、LOOP 指令
正式称为按照 ECX 计数器循环,将程序块重复特定次数。ECX 自动成为计数器,每循环一次计数值减 1。 LOOP 指令的执行有两个步骤: 第一步,ECX 减 1。 第二步,将 ECX 与 0 比较。
mov ax,0
mov ecx,5
L1:
inc ax
loop L1
14、call和ret
call 常用来跳转到子函数 , ret用来返回到原函数
本质上,ret指令用栈中的数据,修改IP的内容,返回代码段的第一条指令。call指令将IP或者CS和IP压入栈中,实现跳转。
pushq %eip # 当前下一条指令入栈,也就是返回地址。
jmpq addr
如上,call先把下一条指令地址入栈,然后跳到子程序执行下一条指令。
函数的标准开头为enter指令,相当于
pushq %rbp # 保存旧的帧指针,相当于创建新的栈帧
movq %rsp, %rbp # 让 %rbp 指向新栈帧的起始位置
subq $N, %rsp # 在新栈帧中预留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位
函数返回前,调用leaveq指令,相当于:
movq %rbp, %rsp
popq %rbp
ret指令,用栈中的数据,修改CS和IP的内容。该指令往往是返回到调用函数调用处的地址。
ret自动用栈中的返回地址修改CS和IP内容。 retq相当于:
pop IP
pop CS
因此,调用ret之前,需要保证栈中下一个元素是返回地址。
计算机内部永远是顺序执行的,通知语句通过改变下一条指令,来改变程序执行的顺序。
assume cs:code
code segment
main: :
:
call sub1//调用子程序sub1
:
mov ax,4c00h
int 21h
sub1: :
:
call sub2//调用子程序sub2
:
ret//子程序返回
sub2: ://子程序sub2开始
:
ret//子程序返回
code ends
end main
其他
1、lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,
例如:lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。
而mov指令则相反,例如:mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。
2、栈的地址比堆高,栈是向下增长的,堆是向上增长的。而对栈来说,增长方向就是栈顶。
3、入栈顺序
C语言函数参数采用自右向左的入栈顺序。 所谓定义,就是在栈或者堆上开辟空间,所谓赋值或者初始化,就是在空间上进行赋值。
栈向低地址方向延申, 因此sub减法常常用于分配内存, 而add则用于回收内存。
寄存器约定
函数调用需要传递参数时,第一个参数存到%edi里,第二个参数会存到%esi里,如果有返回值会存到%eax里
x86-64系统中,用户级别的函数参数通过寄存器传递,%rdi,%rsi,%rdx,%rcx,%r8,%r9依次对应第1参数,第2参数.....
- 每个寄存器的用途并不是单一的。
- %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中。在imul 指令中,两个64位的乘法最多会产生128位的结果,需要 %rax 与 %rdx 共同存储乘法结果,在div 指令中被除数是128 位的,同样需要%rax 与 %rdx 共同存储被除数。
- %rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。
- %rbp 是栈帧指针,用于标识当前栈帧的起始位置
- %rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数(如果有6个或6个以上参数的话)。
- 被标识为 “miscellaneous registers” 的寄存器,属于通用性更为广泛的寄存器,编译器或汇编程序可以根据需要存储任何数据。
函数调用过程
子函数调用时,执行的操作有:父函数将调用参数从后向前压栈 -> 将返回地址压栈保存 -> 跳转到子函数起始地址执行 -> 子函数将父函数栈帧起始地址(%rbp) 压栈 -> 将 %rbp 的值设置为当前 %rsp 的值,即将 %rbp 指向子函数栈帧的起始地址。
保存返回地址和跳转到子函数处执行由 call 一条指令完成,在call 指令执行完成时,已经进入了子程序中,因而将上一栈帧%rbp 压栈的操作,需要由子程序来完成。 注意,子栈帧指针指向的位置,包含的内容是父栈帧的地址。理解这句话。
函数返回过程
函数返回时,我们只需要得到函数的返回值(保存在 %rax 中),之后就需要将栈的结构恢复到函数调用之前的状态,跳转到父函数的返回地址处继续执行。
movq %rbp, %rsp # 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处
popq %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处
注意:在pop %rbp时,%rsp会向上移动一个位置,这个位置内容恰好是返回地址。也就是说,执行完成 ret 后,%rsp 指向的是父栈帧的结尾处,父栈帧尾部存储的调用参数由编译器自动释放。
ESP,EBP
每个任务(进程)有一个栈(可以这样理解一个程序中只有一个栈,每个函数对应一个栈中一段区域即帧),在这个进程中每个函数被调用时分别从这个栈占用一段区域,称为帧(frame)。ESP为栈指针,用于指向栈的栈顶,而EBP为帧指针,指向当前活动记录的顶部。栈指针与帧指针标识出了当前活动记录的位置。
函数执行中%ebp
一直保持不变,可以由此寻址获得函数参数。
保存一个int型数据,只要把%esp指针向下移动4个字节(因为栈增长方向是由高地址到低地址),再根据%esp把该数据移入.
当前函数被调用开始,执作: 1. 将帧指针压入栈中:push ebp 2. 用ebp保存当前栈指针:mov ebp,esp。保证ebp的固定 3. 使得栈指针自减,自减得到的内存应当能够被用来存储被调用函数的本地状态:sub 0CCh,esp
保存返回地址和保存上一栈帧的%rbp 都是为了函数返回时,恢复父函数的栈帧结构。返回值的作用是父函数执行位置,栈帧作用是传入参数。
%esp
寄存器指向当前整个栈的栈顶,%ebp
指向当前帧的帧底,可以认为%ebp
只是存取某时刻的栈顶指针
当前帧的帧底位置保存了上一级调用者的%ebp
指针值(调用者函数帧底),而每个%ebp
的前一个单元存放的就是当前函数的返回地址