🚀 【考纲要求】高级语言程序和机器级代码之间的对应,编译器、汇编器的概念与链接器的基本概念,选择语句的的机器级表示,循环语句的机器级表示,过程(函数)调用对应的机器级表示
🚀 第四章的第一节内容请查看此链接 指令系统4.1-简单熟悉指令系统了解相关概念
🚀 第四章的第二节内容请查看此链接 指令系统4.2-指令的寻址方式和数据的寻址方式
三、程序的机器级代码表示
以一个简单的例子来看什么是程序的机器级代码表示;编写的C语言代码,当我们点击编译器的运行后,电脑就会自动的就将程序运行的结果给输出到屏幕;这一自动的背后,其实是电脑在背后偷偷的为我们干了很多的事情。只有机器语言即01代码才可以直接的电脑中的硬件直接执行;所以当我们的按下运行后,首先将我们编写的高级语言程序代码转变成汇编语言程序,即一条一条的指令;所以程序的机器级代码表示,就是将高级语言程序代码转变成机器级代码(汇编程序),汇编程序经过汇编器后就会变成一段01代码,此时硬件就可以直接运行了。
这一节的内容以X86汇编指令介绍,即就是了解如何将我们编写的高级语言程序用X86这类汇编指令表示出来。
3.1在机器指令中数据在哪?
在前面我们知道了一条指令的组成是操作码+地址码(即数据/数据地址),在4.2节学习了指令寻找数据的方式;那在X86汇编指令中,它是如何表示一条指令以及是怎么区分寻址方式的呢?这就是接下去要学习的-------X86指令寻找数据。
以下以X86中mov指令为例讲解其寻找数据的方式。
①X86处理器寄存器的分类
以下就是X86CPU的所有的寄存器
- EAX EBX ECX EDX ESI EDI EBP ESP 都是32位的,其中EAX EBX ECX EDX是通用寄存器。(E–Extended)
- 为了让通用寄存器更加灵活设置AX BX CX DX分别为EAX EBX ECX EDX的后16位,同时还将其进一步分为AH BH CH DH高八位和AL BL CL DL低八位。
- ESI和EDI是变址寄存器;EBP和ESP是堆栈寄存器。
②寄存器寻址
例如指令 mov eax,ebx
,此条指令实现将寄存器ebx中的内容复制到eax寄存中,而此条指令采用的寻址方式就是寄存器寻址,给出寄存器编号eax和ebx,然后直接读取寄存器中的内容,再将ebx中的内容复制到eax中。
③立即寻址
例如指令mov eax,5
,此条指令实现将立即数5复制到eax寄存中,而此条指令中对5的寻址方式就是立即数寻址,5是立即数。
④直接寻址
dward---两个字(四个字节 32bit)
byte---一个字节(8bit)
word---一个字(两个字节 16bit)
mov eax,dward ptr [ af786h ]
,此条指令实现将主存中地址为af786h的内容读出2个字的内容给eax寄存器。
mov byte ptr [ af786h ],5
,此条指令实现将立即数5复制到主存地址为af786h
的地方占用一个字节。
⑤寄存器间接寻址
mov eax, dword ptr [ebx]
是将寄存中存储的地址取出来找到其在主存中的位置,将32bit的信息复制给eax寄存器。
⑥偏移寻址
mov eax, dword ptr [ ebx+8 ]
将 ebx中的地址偏移8,然后找到其所指主存地址中的32bit信息制到 eax 寄存器中
⑦为指明移动多少位的
mov eax, [ebx]
是将ebx所指向的主存地址的32位(未指明多少位,默认32位)复制进入eax寄存器。
mov [af996h], eax
是将ebx中的内容复制到主存地址为af996h
的中。(未指明读写长度,默认为32bit)
总结:了解了X86指令中的寻址方式不同其不同的表述,同时应该记住8086CPU中寄存器的分类,记住E开头的就是32bit的寄存器,ABCD是四个通用寄存器,同时将其划分为了16位、8位;对于EBP、ESP是堆栈寄存器,ESI、EDI是变址寄存器。
3.2常用的X86汇编指令
在上面我们了解了X86指令的中数据的寻址方式,不难总结出来。对于数据来源无非就是立即数、寄存器、主存,所以指令所需要的数据也是从这三个地方找;接下去将学习X86常用的汇编指令。
在指令系统的第一节内容中,知道了指令的操作类型一共有五种(数据传送类指令、算数和逻辑运算类指令、转移指令、移位指令、输入输出操作)
①算数运算
加 目的操作数 destination 源操作数 source ;其实现的功能就是将s的内容加上d的内容,然后将结果存入d的内容。
add d s
减 将d的内容减去s的内容,然后将结果存放到d中。
sub d s
乘 将d的内容乘上s的内容。最后将结果放入d中。
mul d s
除 对于除法操作来说,其指令中的s表示的是除数,对于被除数其是被隐含表达在edx:eax中,最后的结果其商被放在eax中,余数被放在edx中。
div s
取负数 将d取负数,最后结果放d中
neg d
自增++ 将d++,最后结果放d中。
inc d
自减 - - 将d- - ,最后结果放d中。
dec d
②逻辑运算
与 将d和s逐位相与放回d。
and d s
或 将d和s逐位相或放回d。
or d s
非 将d中的内容逐位取反放回d。
not d
异或 将d和s的内容逐位异或放回d。
xor d s
左移 将d逻辑左移s位,结果放d中。
shl d s
右移 将d逻辑右移,结果放在d中。
shr d s
以上所说的指令都是X86指令,而对于X86指令来说,由于公司不同,语法规范也不同,如下是AT&AT格式和Intel格式的对比。
3.3 选择语句的机器级代码表示
选择语句通过cmp指令和跳转指令来实现的,我们首先要先来学习一系列的跳转指令
je <地址>
若a==b则跳转 #jump when equaljne<地址>
若a!=b则跳转 #jump when not equal,jg <地址>
若a>b则跳转 #jump when greater than,jge <地址>
若a>=b则跳转#jump when greater than or equal to,j1 <地址>
若a<b则跳转#jump when less than,jle<地址>
若a<=b则跳转#jump whenlessthan orequal七o ,
cmp指令:
cmp a,b
#比较a和b两个数
通过cmp比较指令和跳转指令就可以实现所谓的选择语句,如下面这个例子中,就将一个C语言编写的条件判断语句使用机器级代码表示了出来。
cmp eax ebx
比较eax和ebx中数值的大小,其实cmp指令在比较数的大小的是时候在背后做的事情就是将两个数进行减法操作,然后就会产生一系列的标志位OF ZF SF CF等存储在PSW中jg NEXT
jg先判断比较数结果是否为大于,此时只需要取出SF结果符号位的值,就可以知道是否大于,若大于则跳转到NEXT:下的代码执行- NEXT:
mov ecx eax
将eax中的内容送入ecx中,也就是执行if判断中的内容 - jg先判断取出的SF为负号,也就是不大于,则执行
mov ecx ebx
,即执行else中的内容。
也可以如下图所示来实现
3.4 循环语句的机器级代码表示
同样的可以使用cmp指令和跳转指令来实现,如下图所示的例子
- 循环条件是i的值要小于等于100,所以可以先将循环体定义为一个L1:使用cmp指令判断;
cmp edx 100
,让i和100比较 - 若i大于了100,跳出循环段,直接跳L2:
- 若小于等于则,执行循环体
- 循环体执行结束后,判断是否继续循环,
cmp edx 100
,jle L1
,还是小于等于的话继续循环
总结为以下四个步骤:①循环前的初始化②是否直接跳过循环③循环主体
④是否继续循环
这是使用条件转移指令来实现循环语句,在X86机器中,还提供了loop指令来实现循环,直接看例子
- 使用 Looptop: 表示循环的开始,
- loop Looptop: 自动的进行条件判断,若满足条件就会自动的跳转到Looptop:再次进入循环
- 默认是同exc作为循环计数器!!!
3.5 过程调用的机器级代码表示
①高级语言函数调用
先来看一下高级语言的函数调用是如何进行的
- main函数执行是会先开辟一个main函数的调用栈,执行到要使用p()函数的时候
- 开辟一个p的栈帧,继续执行到Q()函数
- 同样的会开辟Q的栈帧,随着Q函数的执行完成,栈帧会被销毁,从而继续执行到函数caller()
- 同样的开辟栈帧caller,执行caller函数的时候执行add函数
- 同样开辟add栈帧,直到add函数执行完成,然后依次返回,销毁栈帧
②机器级代码如何实现函数调用
机器级代码使用call 和 ret 来实现函数的调用以及返回
- 执行caller函数时,执行到call add的指令的时候,说明call指令已经取出完成,此时的IP寄存器指向的应该就是
mov [ebp-4],eax
这条指令。 - call指令执行中会首先将IP值(此时是
mov [ebp-4],eax
这条指令)保存,压入栈顶,同时将IP的值设置为add函数的起始地址。 - 进入add函数首先执行
push ebp 和 mov ebp esp
,将ebp的压栈,同时让ebp指向新的函数add的栈帧基地址 - 接着执行leave命令,是先将esp指向ebp的位置,因为每个开辟栈的栈底,最开始就会先例行执行
push ebp 和 mov ebp esp
指令(或则说是enter指令)保存上一个栈的基地址;此时会在进行pop ebp
,将上一个栈的基地址弹栈赋值给ebp,弹栈后esp会自动+4,指向前一个栈的栈顶。 - ret指令,恢复IP值
完整的实现了使用机器级代码实现函数调用;IP值的压栈,enter:上一个栈帧基地址的保护(存储在新开辟的栈帧的栈底),leave:栈帧的恢复,mov 和pop ,ret恢复保存的IP值。
③如何读取栈帧中的变量值?
pop读取, 这个很好理解
mov指令加上基地址偏移读取
sub esp,12
开辟栈帧大小mov [esp+8],eax
,将eax中的内容放置到esp上两格的位置,也就是放置了211mov [esp+4],985
,将985放置到esp上一格的位置mov eax,[ebp+8]
,将ebp上两格的位置处的数据666赋值进入eax;其实通过该条指令已经实现读取栈帧中元素的数值了。mov [esp],eax
,将eax的值666复制进入主存[esp]中
add esp,8
栈顶指针加8
④如何切换栈帧?
绿色函数执行时候,执行到黄色函数,此时就需要开辟黄色栈帧,在开辟之前,是执行call指令的,call指令的执行首先是将当前IP值压栈,即压入绿色的栈帧,接着call指令还会将IP值改为黄色函数的入口地址;进入黄色函数,首先例行执行enter指令,保护断点(push ebp
和mov ebp esp
);黄色函数执行完成后就需要切换栈帧。
切换栈帧:首先例行执行leave指令,切换栈帧,先mov esp ebp
让esp指回黄色栈底,pop ebp
,出栈的是绿色函数栈帧的基地址复制给ebp,esp自动减4,栈帧切换完成;然后执行ret,返回,恢复IP值。
⑤栈帧有哪些内容
- 例行执行enter 上一个函数的基地址
- 函数内部的局部变量
- 未使用区域
- 可能会调用其他函数,传入其他函数的参数
- IP的返回地址