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语言中的指针。 :::
函数调用: 实参传递(值传递)。