目录
1.汇编指令的构成
2.X86架构CPU中包含的寄存器
3.常见的x86汇编指令
(1)算数运算
(2)逻辑运算
(3)其他
4.AT&T格式
5.选择语句(分支结构)
6.循环语句
(1)条件转移指令实现循环
(2)loop指令实现循环
7.函数调用的机器级指令
如何访问栈帧中的数据
函数调用栈在内存的位置:
访问栈帧数据:push、pop指令:
函数调用时切换栈帧:
恢复esp与ebp的值:
执行ret:
栈帧内可能包含哪些内容
如何传递返回值
高级语言--->汇编语言---->机器语言
一条高级语言可能对应多条汇编语言,但是汇编语言指令与机器语言是一一对应的关系
机器语言与汇编语言合称为机器级代码
1.汇编指令的构成
汇编指令用来改变程序执行流以及处理数据,汇编指令的格式为
操作码+地址码
操作码:告诉机器如何处理
地址码:则告诉机器数据的位置(寄存器:在寄存器中给出"寄存器名",主存:在指令中给出"主存地址",指令:直接在指令中给出操作数,即立即寻址)
以mov指令为例:
其中的内存地址[af996h](h表示16进制)前面的符号:dword ptr,byte ptr,是用来指明内存的读写长度
2.X86架构CPU中包含的寄存器
EAX,EBX,ECX,EDX:通用寄存器
mov eax ebx #寄存器-->寄存器
mov eax dword ptr [af996h] #主存-->寄存器
mov eax,5 #立即数-->寄存器
若想只使用低16bit
mov ax bx #寄存器-->寄存器
mov ax dword ptr [af996h] #主存-->寄存器
mov ax,5 #立即数-->寄存器
也可以使用高低8bit
mov ah bl #寄存器-->寄存器
mov ah dword ptr [af996h] #主存-->寄存器
mov ah,5 #立即数-->寄存器
ESI,EDI:变址寄存器
变址寄存器可用于线性表,字符串的处理
EBP,ESP:堆栈基指针
堆栈寄存器用于实现函数调用
注:只有通用寄存器能使用低16bit或8bit,变址寄存器以及堆栈寄存器只能固定使用32bit
举例:
mov eax,dword ptr [ebx+8]
#将ebx所指的地址偏移8个单位,从这个地址当中复制低32bit到eax寄存器中
mov eax,dword ptr [af996-12h]
#将af996-12所指的贮存地址的低32bit复制到eax寄存器中
3.常见的x86汇编指令
机器识别汇编语言的原理:
CU控制单元会发送控制信号,例如告诉ALU进行算数运算或逻辑运算,ALU就会将输入的数(d,s)进行相应的运算
destination:目的地(d 目的操作数):目的操作数不能为常量
source:来源地 (s 源操作数):可以为一个常量
注:两个操作数不能同时来自于主存,可以同时来自于寄存器
(1)算数运算
对于除法中的被除数edx:eax表示存放64位的被除数,高32位存放在edx,低32位存放在eax中
(2)逻辑运算
(3)其他
用于实现分支结构、循环结构的指令:cmp、test、jmp、jxxx
用于实现函数调用的指令: push、pop、call、ret
用于实现数据转移的指令: mov
4.AT&T格式
AT&T格式与intel格式的区别
AT&T格式是Unix,Linux常用的格式,intel格式是windows常用格式
对于[ebx+ecx*32+4]的使用情景:
5.选择语句(分支结构)
在X86寄存器中,程序计数器PC通常被称为IP,执行一条指令时,PC自动+1,指向下一条即将执行的指令
无条件转移指令:
jmp <地址> #pc无条件转移到<地址>
jmp 128 #<地址>可以用常数给出
jmp eax #<地址>可以来自于寄存器
jmp [999] #<地址>可以来自于主存
若想跳转到某一条指令,就需要知道某条指令的地址,这样是很难的,可以用"标号"瞄准位置
注:这里的NEXT,名字是可以自己取的
无条件转移指令无法实现条件转移,可以使用jxxx
对于比较a,b两个数,需要使用cmp a,b
举个例子:
先else在if
先if后else
补充:cmp的底层原理
比较a,b的大小,本质是进行a-b减法运算
OF (Overflow Flag)溢出标志。溢出时为1,否则置0
SF(Sign Flag) 符号标志。结果为负时置1,否则置0
ZF(Zero Flag)零标志,运算结果为0时ZF位置1,否则置0
CF(Carry Flag)进位/借位标志,进位/借位时置1,否则置0
ALU每次运算的标志位都自动存入PSW程序状态字寄存器中(intel称为标志寄存器)
如下是8086CPU中的16位bit的PSW标志寄存器:
根据标志位的结果,进行判断是否满足jxxx,例如:
jne:若ZF=0,那么满足jne,进行跳转,如果ZF0,那么就不进行跳转
6.循环语句
(1)条件转移指令实现循环
循环语句由4个部分构成:
1.循环前的初始化:
2.是否直接跳过循环:
3.循环主体
4.是否继续循环
(2)loop指令实现循环
这里的loop Looptop等价于
dec ecx
cmp ecx,0
jne Looptop
所以使用loop指令实现的功能一定能用条件转移指令实现
补充:loopx指令--如loopnz,loopz
7.函数调用的机器级指令
函数调用指令:call<函数名>
函数返回指令:ret
例如:
对应的X86指令如下图所示:
call指令的作用:
①将IP寄存器(PC)的IP旧值压栈保存(保存在函数的栈帧顶部)
②设置IP新值,无条件转移到被调用函数的第一条指令
ret指令的作用:
从函数的栈帧顶部找到IP旧值,将其出栈并恢复IP寄存器
如何访问栈帧中的数据
函数调用栈在内存的位置:
在32位系统中,进程虚拟地址空间为4GB,栈低在上,栈顶在下
在常用寄存器中,我们可以看到两个关于栈的寄存器EBP,ESP
EBP与ESP代表的含义如下:EBP指向栈帧的底部,ESP指向栈帧的顶部
当add栈帧执行完后,会退到caller栈帧继续执行,esp与ebp指向的地址也会随之改变
访问栈帧数据:push、pop指令:
push、pop 指令实现入栈、出栈操作,x86 默认以4字节为单位。指令格式如下:
push x:先让esp减4,再将x压入
x可以为立即数,寄存器,主存地址
pop x:栈顶元素出栈写入x,再让esp加4
x可以为寄存器,主存地址
push和pop只能对esp进行访问,但是不能访问栈中其他值,可以使用mov:
•可以用mov 指令,结合 esp、ebp 指针访问栈帧数据
•可以用减法/加法指令,即 sub/add 修改栈顶指针esp 的值
函数调用时切换栈帧:
call指令的作用:
①将IP寄存器(PC)的IP旧值压栈保存(保存在函数的栈帧顶部):效果相当于push ip
②设置IP新值,无条件转移到被调用函数的第一条指令:效果相当于jmp add(add表示标号,程序的执行流会转移到标号位置)
流程如下:
1.push ip,先减4,再将IP旧址压入
2.jmp add,转到add的第一条指令
3.push ebp,先减4,再将ebp指向的值放到栈顶
这里的作用是将ebp指向的地址保存下来,也就是上一层函数的栈帧基址,当执行完当前函数时,可以根据这一地址返回上一层函数
4.mov ebp,esp:让ebp寄存器指向esp所指的地址
让ebp指向当前函数的基地址,也就是设置当前函数的栈帧基址
灰色部分表示原来的ebp和esp,总之完成两件事情,记录上一层函数的基地址,以及将ebp指向当前函数的基地址:
push ebp
mov ebp,esp
这里也可以直接用enter这个指令代替,即上面两条指令等价于:enter
我们可以看到,当前函数的栈帧中的栈底总是存储了上一层函数的基地址,这样执行完当前函数后,就可以根据栈底的地址返回上一层函数
恢复esp与ebp的值:
在执行完当前函数指令后
1.mov esp,ebp #让esp指向当前栈帧底部
2.pop ebp #将esp所指元素出栈,写入寄存器ebp,也就是让ebp重新指回上一函数的栈帧底部,同时esp+4
mov esp,ebp
pop ebp
等价于
leave
执行ret:
恢复esp,ebp的地址后,esp指向了IP的旧值,那么继续执行ret
ret的作用:从函数的栈帧顶部找到IP旧值,将其出栈并恢复IP寄存器,也就是让程序的执行流回到call指令的下一条指令:
总结如下:
栈帧内可能包含哪些内容
对于以下函数,栈帧存储的内容:
1.栈帧底部一定是上一层栈帧基地址(ebp旧值)
2.在调用下一层函数时,一定会使用到call指令,call指令会将IP寄存器的值压到栈顶保存,所以IP寄存器的值(返回地址)保存在栈帧顶部
3.将局部变量集中存储在栈帧底部区域,C语言中越靠前定义的局部变量越靠近栈顶
如何访问局部变量:
这里只需要将[ebp-4]=sum,[ebp-8]=temp2,[ebp-12]=temp3,[ebp-4]表示最后一个定义的局部变量
4.将调用参数集中存储在栈帧顶部区域,参数列表中越靠前的参数越靠近栈顶
如何访问调用参数:
这里只需要将[ebp+8],得到第一个参数,[ebp+12],得到第二个参数
5.栈帧中可能出现空闲未使用的区域
在gcc 编译器中,将每个栈大小设置为 16B 的整数倍(当前函数的栈除外),因此栈帧内可能出现空闲未使用的区域。
例如add栈帧可以为4B,8B,但是只要这个函数需要调用下一层的函数就必须凑齐16B的整数倍
总结:
如何传递返回值
多个参数可以通过函数调用栈传递,但是函数的返回值只有一个,所以通常将返回值保存到eax寄存器中,所以当返回到上一层函数时,只需要到eax中取结果即可
总结:
补充:调用其他函数前,如果有必要,可将某些寄存器 (如: eax、edx、ecx)的值入栈保存,防止中间结果被破坏。
本篇总结: