CPU编译后的可执行程序是项目名称命名的exe文件。
汇编指令分为两个类型:
操作码字段:表征指令的操作特性和功能,是指令的唯一标识。(要做什么操作?)
地址码字段:指定参与操作的操作数的地址码。(操作哪里的内存?)
指令中指定操作数存储位置的字段称为地址码,地址码中可以包含存储器地址,也可包含寄存器编号。
指令中可以有一个、两个或者三个操作数,也可没有操作数,根据一条指令有几个操作数地址,可将指令分为零地址指令、一地址指令、二地址指令、三地址指令。
二地址指令格式中,从操作数的物理位置来说有可归为三种类型:
①寄存器-寄存器(RR)型指令:需要多个通用寄存器或个别专用寄存器,从寄存器中取操作数,把操作结果放入另一个寄存器,机器执行寄存器-寄存器型的指令非常快,不需要访问内存。
②寄存器-存储器(RS)型指令:执行此类指令时,既要访问内存单元,又要访问寄存器。
③存储器-存储器(SS)型指令:操作时都是涉及内存单元,参与操作的数都是放在内存里,从内存某单元中取操作数,操作结果存放至内存另一单元中,因此机器执行指令需要多次访问内存。
(寄存器:register;存储器:storage。)
( test.c main.c ) --1--> (test.i main.i) --2--> (test.s main.s) --3--> (test.o main.o) --4--> (a.out) ::: tip C代码编译成可执行程序经过4步: (1)预处理:展开头文件、宏替换、删除代码注释、条件编译,不检查语法(.1) (2)编译:检查语法,将预处理后文件编译生成汇编文件(.s) (3)汇编:汇编代码转换成机器码(.obj) (4)链接:链接调用的库函数生成可执行文件(.exe) :::
VS生成汇编的设置步骤:
VS的汇编后缀是cod。
Clion生成汇编的步骤:
底边栏“终端”-输入“gcc (-m32 -masm=intel)-S -fverbose-asm 文件名.c”,回车生成文件名.s的汇编文件。
Clion生成机器码的步骤: gcc -m32 -g -o 文件名 文件名.c objdump --source 文件名.exe >文件名.dump
汇编常用指令
汇编指令可以分为数据传送指令、逻辑计算指令、控制流指令。
::: warning
在汇编中,寄存器访问内存时需要加方括号。
:::
Intel汇编指令:
数据传送指令:
①mov指令:将第二个操作数(寄存器的内容、内存中的内容或常数值)复制到第一个操作数(寄存器或内存)。但不能用于直接从内存复制到内存。
②push指令:将操作数压入内存的栈,常用于函数调用。ESP是栈顶,压栈前先将ESP值减4(栈增长方向与内存地址增长方向相反),然后将操作数压入ESP指示的地址。
③pop指令:与 push 指令相反,pop指令执行的是出栈工作,出栈前先将ESP指示的地址中的内容出栈,然后将 ESP值加4。
算术与逻辑运算指令:
①add/sub指令:add指令将两个操作数相加,相加的结果保存到第一个操作数中。sub指令用于两个操作数相减,相减的结果保存到第一个操作数中。
②inc/dec指令:inc、dec 指令分别表示将操作数自加1、自减1。
③imul指令:带符号整数乘法指令,有以下两种格式:
两个操作数,将两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器;
三个操作数,将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。
乘法操作结果可能溢出,则编译器置溢出标志OF=1,以使CPU调出溢出异常处理程序。
④idiv 指令:带符号整数除法指令,只有一个操作数即除数,而被除数则为edx:eax
中的内容(64位整数),操作结果有两部分:商和余数。商送到eax,余数则送到edx。
⑤and/or/xor指令。and、or、xor指令分别是按位与、按位或、按位异或操作指令,用于操作数的位操作(按位与、按位或、按位异或),操作结果放在第一个操作数中。
⑥not指令:位翻转指令,将操作数的每一位翻转,即0→1、1→0。
⑦neg指令:取负指令。
⑧shl/shr指令。逻辑移位指令,shl为逻辑左移,shr为逻辑右移,第一个操作数表示被操作数,第二个操作数指示移位的位数。
⑨lea指令:地址传送指令,将有效地址传送到指定的的寄存器。
控制流指令:
x86处理器维持着一个指示当前执行指令的指令指针(IP),当一条指令执行后,此指针自动指向下一条指令。IP寄存器不能直接操作,但可以用控制流指令更新。通常用标签(label)指示程序中的指令地址,在x86汇编代码中,可在任何指令前加入标签。
如上图使用begin (begin代表标签名,可以为别的名字)指示了第二条指令,控制流指令通过标签就可以实现程序指令的跳转。
①jmp指令:jmp指令控制IP转移到label所指示的地址(从label中取出指令执行)。
②jconditio指令:条件转移指令,依据CPU状态字中的一系列条件状态转移。CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。类似关系运算符。
③cmp/test指令。cmp指令用于比较两个操作数的值;test指令对两个操作数进行逐位与运算。
这两类指令都不保存操作结果,仅根据运算结果设置CPU状态字中的条件码。
cmp和test指令通常和jcondition指令搭配使用。
④call/ret指令:分别用于实现子程序(过程、函数等)的调用及返回。
::: tip
call指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其他简单的跳转指令不同,call指令保存调用之前的地址信息(当call指令结束后,返回调用之前的地址)。ret指令实现子程序的返回机制,ret指令弹出栈中保存的指令地址,然后无条件转移到保存的指令地址行。call和ret是程序(函数)调用中最关键的两条指令。
:::
条件码
编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。
条件码(标志位)
除了整数寄存器,CPU还维护着一组条件码(标志位)寄存器,它们描述了最近的算术或逻辑运算操作的属性。可以检测这些寄存器来执行条件分支指令。
常用条件码如下:
OF和SF对无符号数运算来说没有意义,而CF对带符号数运算来说没有意义。
判断是否溢出最简单的是如果正数相加结果变为负数则溢出,负数相加变正数则溢出。
两个十六进制数判断是否溢出:
即数据位高位和符号位高位进位不一样的时候会发生溢出。
常见的算术逻辑运算指令(add、sub、imul、or、and、shl、inc、dec、not、sal 等)会设置条件码。但有两类指令只设置条件码而不改变任何其他寄存器,即cmp和test指令,cmp指令和sub指令的行为一样,test指令与and指令的行为一样,但它们只设置条件码,而不更新目的寄存器。
变量赋值汇编
CPU运行C代码时去掉了变量名,数据是从一个空间拿到另一个空间的过程,即编译后的代码中没有变量名。
访问所有变量的空间都是通过栈指针(esp存放,也可称为栈顶指针)的偏移来获取对应变量内存空间的数据。
call调用函数。
mov将第二个操作数复制到第一个操作数的位置。
arr中每一个整型元素占4个字节空间,三个元素的位置分别是距离栈顶指针24、28、32字节。
(esp存了main函数栈顶指针内存地址,每次+1偏移一个字节)
::: tip
英特尔汇编中,DWORD表示4个字节,word表示2个字节,byte表示1个字节。
:::
DWORD PTR可以看作是强制类型转换,表示将元素1存在栈顶指针偏移后指向的起始位置向后四个字节的位置(一个整型的大小)。
将常量5存放在esp存放的栈指针向后偏移44字节的位置,即变量i的空间。
将常量10存放在esp存放的栈指针向后偏移40字节的位置,即变量j的空间。
先将[esp+32]内的数据放进寄存器eax中,再把eax中的值放进内存空间[esp+44]。
(汇编指令不支持内存到内存的拷贝,不能将[esp+32]直接拷贝到[esp+44])
汇编指令如为mov eax,DWORD PTR [esp+24],意为指向[esp+24]开始向下4个字节的内存空间内。
lea eax,[esp+24]可以取到[esp+24]指向的内存空间的地址,取地址时不关心类型,不需要DWORD PTR等。
将[esp+24]的地址放进寄存器eax中。eax编译器备注为tmp90,将tmp90放进p中。
::: warning 在英特尔的汇编中,针对内存时会加DWORD PTR,寄存器间的操作因为寄存器长度是固定的WORD,不需要加WORD。 :::
选择循环汇编
#include <stdio.h>
int main() {
int i=5;
int j=10;
if(i<j)
{
printf("i is small\n");
}
for(i=0;i<5;i++)
{
printf("this is loop\n");
}
return 0;
}
text表示文字常量区,存放着字符串常量,LC0和LC1分别表示两个字符串常量的地址。
intel汇编不能操作两个内存,先将变量i放进eax寄存器,然后使用cmp指令比较大小,前一个操作数减后一个操作数,然后设置条件码(条件码是CPU设置的,汇编中看不到)
jge L2表示如果前一个操作数大于等于后一个操作数,即eax寄存器大于等于DWORD PTR [esp+24]时,跳转到L2,否则继续向下执行,jge是根据条件码ZF和SF判断的。jge是条件转移指令,为if服务。
将0赋值给i后直接跳转L3。
函数调用汇编
此时esp是main函数的栈基指针
将a的地址赋给p,先将a的地址放在寄存器eax中,再把eax中的值赋给[esp+28]。
间接访问
DWORD PTR [esp+28]的地址值存放进寄存器eax,再将地址eax放在方括号内取出地址存放的值赋给eax。
p存放a的地址,即[esp+28]存放a的地址,再交给eax后,eax存放a的地址,[eax]取到eax中地址指向的值即a的值,赋给eax的值为5。
::: tip
加[ ]表示间接取操作数,类似C语言中的指针。
:::
函数调用:
实参传递(值传递)。