汇编语言学习笔记
x86-64(也叫x64或amd64)是x86/IA32指令集的64-bit版本。
可以使用NASM,它针对x86系列处理器:
1 |
sudo apt-get install nasm |
很多指令会带有b w l q后缀,分别表示操作寄存器的1 2 4 8字节。
指令 | C助记符 | 说明 | ||||
赋值操作 | ||||||
mov src, dst | dst = src |
从源复制值到目标,源可以是一个直接量、寄存器、内存地址,目标可以是寄存器或内存地址。源/目标不能同时是内存地址 movb movw movl movq分别表示移动1 2 4 8字节(整数的低位)
|
||||
movs |
mov时,如果是位宽小的数据拷贝到位宽大的目标,则使用sign-extend填充额外字节
|
|||||
cltq | 专门操作%rax,等价于 movslq %eax,%rax | |||||
movz | mov时,如果是位宽小的数据拷贝到位宽大的目标,则使用zero填充额外字节
需要注意:用mov将32bit值拷贝到寄存器时,高32bit自动清零,相当于自动movz。下面的指令:
看似无意义,实际上会将高32bit清零 |
|||||
lea src, dst | dst = src | 即load effective address指令,源是一个内存地址,将计算得到的地址(不是数据)拷贝到目标:
该指定不会对源地址进行解引用操作 |
||||
算术操作 | ||||||
add src, dst | dst += src | |||||
sub src, dst | dst -= src | |||||
imul src, dst | dst *= src | imul src这种特殊用法,假设乘法的另外一个操作数位于%rax,计算得到128bit结果,高64bit存放到%rdx | ||||
neg dst | dst = -dst | |||||
and src, dst | dst &= src | |||||
or src, dst | dst |= src | |||||
xor src, dst | dst ^= src | 按位异或 | ||||
not dst | dst = ~dst | 按位取反 | ||||
shl count, dst | dst <<= count | 左移,同义词sal | ||||
sar count, dst | dst >>= coun | 算术右移 | ||||
shr count, dst | dst >>= coun | 逻辑右移 | ||||
条件分支 | ||||||
cmp op2, op1 | result = op1 - op2 |
属于算术指令,计算结果丢弃,在%eflags中设置条件代码(结果为0也就是操作数相等则ZF置1)。操作数可以是直接量、内存地址(最多一个操作数)、寄存器 |
||||
test op2, op1 | result = op1 & op2 | 属于逻辑指令,计算结果丢弃,在%eflags中设置条件代码(结果为0则ZF置1)。操作数可以是直接量、内存地址(最多一个操作数)、寄存器 | ||||
jmp target | 无条件跳转 | |||||
je target | if (ZF=1) goto | 如果相等,则跳转 | ||||
jne target | if (ZF=0) goto | 如果不等,则跳转 | ||||
jl target | if (SF!=OF) goto | 如果小于,则跳转 | ||||
jle target | if (ZF=0 || SF!=OF) goto | 如果小于等于,则跳转 | ||||
jg target | if (ZF=0 and SF=O) goto | 如果大于,则跳转 | ||||
jge target | if (SF=OF) goto | 如果大于等于,则跳转 | ||||
js | if (SF=1) goto | |||||
jns | if (SF=0) goto | |||||
sete dst | 如果ZF=1,则将dst寄存器的值设置为1 | |||||
setge dst | 如果大于等于,则将dst寄存器的值设置为1 | |||||
函数调用栈 | ||||||
push dst |
将直接量、寄存器或者内存地址压入栈中,操作数变成新的栈顶,%rsp寄存器的值因而递减
|
|||||
pop dst |
弹出栈顶的值,存入目标寄存器,%rsp寄存器的值因而递增
|
|||||
callq funcaddr | funcaddr() | 通过地址调用指定的函数。它指令将返回地址(当前函数下一条指令的地址)压栈(%rip),然后跳转到目标函数上继续运行 | ||||
retq | return | 弹出栈顶作为%rip的值,导致当前函数调用返回 |
常用的寄存器包括16个通用寄存器,两个特殊用途寄存器。每个寄存器都是64bit大小,通过一个伪名,可以饮用低32/16/8位部分。
下表列出这些寄存器的惯用法。-owned标注出所有者,所有者(函数调用时)可以自由使用寄存器,覆盖它的值,非所有者则必须复制值到其它地方才能安全使用
寄存器 | 惯例用法 | 低32位 | 低16位 | 低8位 |
%rax | 用于返回值,被调用者拥有 | %eax | %ax | %al |
%rdi | 1st argument, callee-owned | %edi | %di | %dil |
%rsi | 2nd argument, callee-owned | %esi | %si | %sil |
%rdx | 3rd argument, callee-owned | %edx | %dx | %dl |
%rcx | 4th argument, callee-owned | %ecx | %cx | %cl |
%r8 | 5th argument, callee-owned | %r8d | %r8w | %r8b |
%r9 | 6th argument, callee-owned | %r9d | %r9w | %r9b |
%r10 | Scratch/temporary, callee-owned | %r10d | %r10w | %r10b |
%r11 | Scratch/temporary, callee-owned | %r11d | %r11w | %r11b |
%rsp |
用于栈指针,调用者拥有。push/pop指令添加/删除栈的元素 直接操作该寄存器,可以实现添加、删除一系列变量 注意:栈是向下增长的,也就是向低地址方向增长 |
%esp | %sp | %spl |
%rbx | Local variable, caller-owned | %ebx | %bx | %bl |
%rbp | Local variable, caller-owned | %ebp | %bp | %bpl |
%r12 | Local variable, caller-owned | %r12d | %r12w | %r12b |
%r13 | Local variable, caller-owned | %r13d | %r13w | %r13b |
%r14 | Local variable, caller-owned | %r14d | %r14w | %r14b |
%r15 | Local variable, caller-owned | %r15d | %r15w | %r15b |
%rip |
指令寄存器,指向下一条需要执行的指令地址 调用函数时,该寄存器的值被压栈,以便函数调用完毕后能够返回 |
|||
%eflags |
Status/condition code bits 这个特殊的寄存器,用于存放一系列的boolean标记位 —— 条件代码(condition codes),大部分算术运算符都会更新这些代码 条件跳转(conditional jump)指令会读取条件代码,来确定是否跳转到某个分支 条件代码包括: ZF 零标记,如果上一次比较/算术操作符产生“相等”或零,则设置为1 |
由于CISC的特性,x86-64支持多种不同的地址模式。地址模式用于计算内存地址,以便读写之。下面的汇编指令说明了如何向各种地址模式写入数据:
1 2 3 4 5 6 7 8 9 10 11 12 |
movl $1, 0x604892 ; 直接地址,地址是常量 movl $1, (%rax) ; 间接地址,地址存放在寄存器中 movl $1, -24(%rbp) ; 直接地址+置换 base %rbp + displacement -24 movl $1, 8(%rsp, %rdi, 4) ; indirect with displacement and scaled-index (address = base %rsp + displ 8 + index %rdi * scale 4) movl $1, (%rax, %rcx, 8) ; (special case scaled-index, displ assumed 0) movl $1, 0x8(, %rdx, 4) ; (special case scaled-index, base assumed 0) movl $1, 0x4(%rax, %rcx) ; (special case scaled-index, scale assumed 1) |
通过callq指令可以发起函数调用,被调用函数中使用retq指令来返回。
调用者必须将前6个参数存入%rdi, %rsi, %rdx, %rcx, %r8, %r9这些寄存器中。如果参数超过6个,额外的参数压栈。
1 2 3 |
mov $0x3, %rdi ; 第一个参数存入 %rdi mov $0x7, %rsi ; 第二个参数存入 %rsi callq binky ; 移交控制权给binky函数 |
被调用函数运行结束后,它应该将返回值写入到%rax,并清理掉栈,最后使用retq指令:
1 2 3 |
mov $0x0, %eax ; 设置返回值 add $0x10, %rsp ; 解除栈分配 retq ; 从当前函数返回,回到caller继续执行 |
条件分支、静态跳转、函数调用,都会导致跳转执行。
跳转执行的目标,通常是在编译阶段就确定下来的绝对地址。 但是,很多情况下,目标仅到运行时才能知道,这种情况下目标地址被编译器存放在寄存器中。
Leave a Reply