x86汇编语言指令基础
x86汇编语言是指所有结尾为86型号的cpu所能解析的语言。
指令的格式为:操作码 + 地址码。
以mov操作(mov指令功能,将源操作数s复制到目的操作数d所指的位置)为例:
mov eax,ebx:意思是将寄存器ebx的值复制到寄存器eax中。
mov eax,5: 意思是将立即数5复制到寄存器eax中。
mov eax, dword ptr [af996h]:意思是将内存地址 af996h所指的32bit值复制到寄存器eax中。
mov byte ptr [af996h], 5:意思是将立即数5复制到内存地址 af996h所指的一字节中。
内存读写长度的指明:
dword ptr——双字, 32bit
word ptr——单字,16bit
byte ptr——字节,8bit
x86架构相关寄存器
若通用寄存器去掉开头的英文字母E,即AX,BX,CX,DX,此时大小为低16bit。
若未指明主存读写长度时,默认32bit。
例: mov eax, [ebx] 等价于 mov eax, dword ptr [ebx]
常用指令
以下的s均为源操作数(source),d为目的操作数(destination)。且目的操作数不可为常量,x86中两操作数不允许同时来自内存中。
常见的算术运算指令
add d,s:add,加,把s加到d中。
sub d,s:subtract,减,d中减去s。
mul d,s:multiply,乘,无符号数d*s,乘积存入d。
imul d,s:有符号数d*s,乘积存入d。
div s:divide,除,无符号数除法,被除数会隐含放入到edx:eax(将被除数位扩展)中,然后edx:eax / s,商存入eax,余数存入edx。
idiv s:有符号数除法,被除数会隐含放入到edx:eax中,然后edx:eax / s,商存入eax,余数存入edx。
neg d:negative,将d取负数,结果存入d。
inc d:increase,d++。
dec d:decrease,d–。
常见的逻辑运算指令
and d,s:and,将d、s逐位相与,结果放回d。
or d,s:or,将d、s逐位相或,结果放回d
not d:not,将d逐位取反,结果放回d
xor d,s:exclusive or,将d、s逐位异或,结果放回d
shl d,s:shift left,将d逻辑左移s位,结果放回d(通常s是常量)
shr d,s:shift right,将d逻辑右移s位,结果放回d(通常s是常量)
其他指令
用于实现分支结构、循环结构的指令:cmp、test、jmp、jxxx
用于实现函数调用的指令:push、pop、call、ret
用于实现数据转移的指令:mov
AT&T格式和Intel格式
AT&T制定的汇编语言格式通常用于Unix、linux
Intel制定的汇编语言格式通常用于windows
选择语句的机器级表示
注:在Intel x86处理器中,程序计数器PC(Program Counter)通常被称为IP(Instruction Pointer)。
无条件转移指令——jmp
无条件转移指令格式: jmp <地址>
使用jmp会让PC无条件转移至<地址>,地址可以是常量、寄存器或来自主存。
例:
jmp 128:跳转到地址为128的位置
jmp eax:跳转到eax寄存器中所指的位置
jmp [999]:跳转到主存地址999中所指的位置
除了以上方法使用jmp,也可以使用“标号”锚定位置(用冒号结尾,名字可以自己取)。
条件转移指令——jxxx
条件转移指令一般要和cmp
指令一起使用
cmp a,b #比较a和b两个数
je <地址> #jump when equal,若a==b跳转
jne <地址> #jump when not equal,若a!=b跳转
jg <地址> #jump when greater than,若a>b跳转
jl <地址> #jump when less than, 若a>=b跳转
jle <地址> #jump when less than or equal to,若a<=b跳转
例:
若有这么一段C语言代码
if (a>b){
c=a;
} else {
c = b;
}
则用汇编表示
mov eax,a #将变量a存入eax
mov ebx,b #将变量b存入ebx
cmp eax,ebx #比较a和b的值
jg NEXT #若a>b,转移到NEXT:
mov ecx,ebx #假设ecx存储变量c,令c=b
jmp END #无条件转移到END:
NEXT:
mov ecx,eax #假设ecx存储变量c,令 c=a
END:
循环语句的机器级表示
例
有这么一段循环代码
int i = 1;
int result = 0;
while(i<=100){
result += i;
i++;
} //求 1+2+3+...+100
上方代码用汇编实现,即
mov eax,0 #用eax保存result,初始值为0
mov edx,1 #用edx保存 i,初始值为1
cmp edx,100 #比较 i和100
jg L2 #若i>100,转跳到L2执行
L1: #循环主体
add eax,edx #实现 result +=i
inc edx #inc 自增指令,实现i++
cmp edx,100 #比较 i和100
jle L1 #若 i<=100,则转跳到L1执行
L2: #跳出循环主体
用loop指令实现循环
x86中还提供了loop
指令来实现循环
例:
for(int i=500; i>0; i--){
//coding...
}
用汇编实现
mov ecx,500 #用ecx作为循环计数器
Looptop: #循环的开始
...
coding...
...
loop Looptop #ecx--,若ecx!=0,跳转到Looptop
上方loop Looptop
指令等价于dec ecx cmp ecx,0 jne Looptop
三条指令。
注:loop是默认对ecx进行操作,用loop通用寄存器只能用ecx作为循环计数器
函数调用的机器级表示
在高级语言中,每个函数都有自己的栈区,称为栈帧,一个栈由若干个栈帧组成。目前正在执行的函数栈帧是一定位于栈顶。
函数调用x86汇编语言一般会通过call/ret
指令来实现。
call/ret指令
用于实现子程序的调用及返回。
例:
int main(){
//coding...
add();
return 0;
}
x86汇编实现
main:
...
coding...
...
call add #这里调用了call函数
...
ret #main函数结束
call指令的作用:
- 将IP旧值压栈保存(效果相对于push IP)
- 设置IP新值,无条件转移至被调用函数的第一条指令(效果相对于 jmp xxx)。
函数调用栈在内存中的位置
若有一台32位x86系统(x86系统默认以4字节为栈的操作单位),进程虚拟地址空间位4GB,高地址的1GB为操作系统内核区,低地址3GB为用户区,函数调用栈在用户区的高地址部分,具体如图所示。
若想使用栈,则需要用到EBP(堆栈基指针),ESP(堆栈顶指针)通用寄存器,EBP指向当前栈帧的“底部”,ESP指向当前栈帧的“顶部”。
访问栈帧数据
使用push和pop指令访问
若要操作栈,则需使用push
和pop
指令,push
会先让esp减4(即指向低地址的数据),后可以将立即数、寄存器或主存地址压入到栈顶,pop
则是将栈顶元素出栈,然后放到寄存器或主存地址中,再让esp加4(指向高地址的数据)。
例:
push eax #将寄存器eax的值压栈
push 985 #将立即数985压栈
push [ebp+8] #将主存地址[ebp+8]里的数据压栈
pop eax #栈顶元素出栈,写入寄存器eax
pop [ebp+8] #栈顶元素出栈,写入主存地址[ebp+8]
使用mov指令访问
例:
sub esp,12 #栈顶指针-12
mov [esp+8],eax #将eax的值复制到主存[esp+8]中
add esp,8 #栈顶指针+8
即可以用mov指令,结合esp、ebp指针访问栈帧数据,用加法/减法指令,即sub/add修改栈顶指针esp的值。
函数调用时,切换栈帧
当函数调用时,可以通过push ebp
和mov ebp,esp
来切换栈帧。
例:
有这么一段汇编代码
caller:
push ebp
mov ebp,esp
...
call add
...
leave
ret
...
...
add:
push ebp
mov ebp,esp
...
...
leave
ret
当上方代码运行到call add
时,就会调用add函数,此时就需要切换栈帧,call指令一使用就会将IP旧值压栈,然后将新的IP值指向函数内的第一条指令,即push ebp
,将栈低地址压入栈顶中,此时就是进行切换栈帧。
执行mov ebp,esp
,将ebp
栈底寄存器指向栈顶,就实现了切换。
注:push ebp
和mov ebp,esp
可以精简为enter
一条指令,效果相同
若想恢复原先的栈帧,使用mov esp,ebp
和pop ebp
即可实现,这两条也等价于leave
指令。
栈帧中可能包含的内容
通常会将局部变量集中存储在栈帧底部区域。
通常将调用参数集中存储在栈帧顶部区域。
栈帧最底部一定是上一层栈帧基址(ebp旧值)
栈帧最顶部一定是返回地址(当前函数的栈帧除外)
使用ebp-4
或ebp-8
等就可以访问局部变量,用esp+4
或esp+8
等就可以访问调用参数
gcc编译器将每个栈帧大小设置为16B的整数倍(当前函数的栈帧除外),所以可能会出现中间空闲区域。