目录

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,进行跳转,如果ZF\neq0,那么就不进行跳转

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)的值入栈保存,防止中间结果被破坏。


本篇总结:

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐