【408篇】C语言笔记-第二十一章(汇编语言)

第一节:汇编指令格式讲解

1. 汇编指令格式

CPU是如何执行我们的程序的?

我们编译后的可执行程序,也就是main.exe,是放在代码段的,PC指针寄存器存储了一个指针,始终指向要执行的指令,读取了代码段的某一条指令后,会交给译码器来解析,这时候译码器就知道要做什么事了,CPU中的计算单元,加法器不能直接对栈上的某个变量a,直接做加1的操作,需要首先将栈,也就是内存上的数据,加载到寄存器中,然后再利用加法器加1操作,在从寄存器搬到内存上去。

CPU读写寄存器的速度比读写内存的速度要快很多

操作码字段:表征指令的操作特征与功能(指令的唯一标识),不同的指令操作码不能相同。

地址码字段:指定参与操作的操作数的地址码。

指令中指定操作数存储位置的字段称为地址码,地址码中可以包含存储区地址。也可以包含寄存器编号。

指令中可以有一个、两个或者三个操作数,也可以没有操作数,根据一条指令有几个操作数地址,可以将指令分为零地址指令,一地址指令,二地址指令,三地址指令。4个地址码的指令很少被使用。

零地址指令:只有操作码,没有地址码(空操作,停止等)。

一地址指令:指令编码中只有一个地址码,指出了参加操作的一个操作数的存储位置,如果还有另一个操作数则隐含在累加器中。

二地址指令:指令编码中有两个地址,分别指出了参加操作的两个操作数的存储位置,结果存储在其中一个地址中。

三地址指令:指令编码中有3个地址码,指出了参加操作的两个操作数的存储位置和一个结果的地址。

二地址指令格式中,从操作数的物理位置来说可归为三种类型。

寄存器英文:register

存储器英文:storage

寄存器-寄存器(RR)型指令:需要多个通用寄存器或个别专用寄存器,从寄存器中取操作数,把操作结果放入到另一个寄存器,执行速度非常快,不需要访问内存。

寄存器-存储器(RS)型指令:执行此类指令时,既要访问内存单元,又要访问寄存器。

存储器-存储器(SS)型指令:操作时都是涉及内存单元,参与操作的数都是放在内存里,从内存某单元中取操作数,操作结果存放至内存另一个单元中,因此机器执行此指令需要多次访问内存。

复杂指令集:变成 x86 CISI (一般是PC端)

精简指令集:等长 arm TISI (一般是手机端)

2. 生成汇编方法

编译过程:

第一步:main.c --> 编译器 --> main.s 文件(.s文件就是汇编文件,文件内是汇编代码)

第二步:main.s 汇编文件 --> 汇编器 --> main.obj 文件

第三步:main.obj文件 --> 链接器 --> 可执行文件 exe

方法步骤:(以CLion为例)

1.配置环境变量。

说明:Mac电脑无需设置环境变量,直接进行下面的操作。

2.完成C代码的编写,在终端窗口输入gcc -S -fverbose-asm main.c,就可以生成汇编文件main.s。

说明:安装MC68000插件,汇编代码即可高亮显示。

第二节:汇编常用指令讲解

1. 相关寄存器

除EBP和ESP外,其他几个寄存器的用途是比较任意的,也就是什么都可以存

2. 常用指令

汇编指令通常可以分为数据传送指令、逻辑计算指令和控住流指令。以Interl为例:

<1>数据传送指令:

1)mov指令。将第二个操作数(寄存器的内容,内存中的内容或常数值)复制到第一个操作数(寄存器或内存),但不能直接从内存复制到内存。

语法如下:

mov <reg>,<reg>
mov <reg>,<mem>
mov <men>,<reg>
mov <reg>,<con>
mov <mem>,<con>

举例:

mov eax, ebx         # 将ebx值复制到eax
mov byte prt [var],5 # 将5保存到var值指示的内存地址的一字节中

2)push指令。将操作数压入内存的栈,常用于函数调用。ESP是栈顶,压栈前先将ESP值减4(栈增长方向与内存地址增长方向相反),然后将操作数压入ESP指示的地址。

语法如下:

push <reg32>
push <mem>
push <con32>

举例:(注意,栈中元素固定为32位)

push eax   # 将eax压入栈
push [var]  # 将var值指示的内存地址的4字节值压入栈

3)pop指令。与push指令相反,pop指令执行的是出栈工作,出栈前先将ESP指示的地址中的内容出栈,然后将ESP值加4.

语法如下:

pop edi   # 弹出栈顶元素送到edi
pop [ebx] # 弹出栈顶元素送到ebx值指示的内存地址的4字节中

<2>算术和逻辑指令:

1)add/sub指令。add指令将两个操作数相加,相加的结果保存到第一个操作数中。sub指令用于两个操作数相减,相减的结果保存到第一个操作数中。

语法如下:

add <reg>,<reg> / sub <reg>,<reg>
add <reg>,<mem> / sub <reg>,<mem>
add <mem>,<reg> / sub <mem>,<reg>
add <reg>,<con> / sub <reg>,<con>
add <mem>,<con> / sub <mem>,<con>

举例:

sub eax,10  # eax <- eax-10
add byte prt [var],10 # 10与var值指示的内存地址的一字节值相加,并将结果保存在var值指示的内存地址的字节中

2)inc/dec指令。inc、dec指令分别表示将操作数自加1、自减1。

语法如下:

inc <reg> / dec <reg>
inc <mem> / dec <mem>

举例:

dec eax   # eax值自减1
inc dword ptr [var] # var值指示的内存地址的4字节值加1

3)imul指令。带符号整数乘法指令,有两种形式:1.两个操作数。将两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。2.三个操作数。将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。

语法如下:

imul <reg32>,<reg32>
imul <reg32>,<mem>
imul <reg32>,<reg32>,<con>
imul <reg32>,<mem>,<con>

举例:

imul eax,[var]  # eax <- eax * [var]
imul esi,edi,25 # esi <- edi * 25

乘法操作结果有可能溢出,则编译器溢出标志OF=1,以使CPU调出溢出异常处理程序。

4)idiv指令。带符号整数除法指令,它只有一个操作符,即除数,而被除数则为edx:eax中的内容(64位整数),操作符结果有两部分:商和余数,商送到eax,余数送到edx。

语法如下:

idiv <reg32>
idiv <mem>

举例:

idiv ebx
idiv dword ptr [var]

5)and/or/xor指令。and、or、xor指令分别是按位与、按位或、按位异或操作指令,用于操作数的位操作(按位与、按位或、异或),操作结果放在第一个操作符中。

语法如下:

and <reg>,<reg> / or <reg>,<reg> / xor <reg>,<reg>
and <reg>,<mem> / or <reg>,<mem> / xor <reg>,<mem>
and <mem>,<reg> / or <mem>,<reg> / xor <mem>,<reg>
and <reg>,<con> / or <reg>,<con> / xor <reg>,<con>
and <mem>,<con> / or <mem>,<con> / xor <mem>,<con>

举例:

and eax,0fH  # 将eax中的前28位全部置为0,最后4位保持不变
xor edx,edx  # 置edx中的内容为0

6)not指令。为翻转指令,将操作数中的每一位翻转,即0->1,1->0。

语法如下:

not <reg>
not <mem>

举例:

not byte ptr [var] # 将var值指示的内存地址的一字节的所有位翻转

7)neg指令。取负指令。

语法如下:

neg <reg>
neg <mem>

举例:

neg eax  # eax <- -eax

8)shl/shr指令。逻辑移位指令,shl为逻辑左移,shr为逻辑右移,第一个操作数表示被操作数,第二个操作数指示位移的位数。

语法如下:

shl <reg>,<con8> / shr <reg>,<con8>
shl <mem>,<con8> / shr <mem>,<con8>
shl <reg>,<cl> / shr <reg>,<cl>
shl <mem>,<cl> / shr <mem>,<cl>

举例:

shl eax,1  # 将eax值左移1位,相当于乘以2
shr ebx,cl # 将ebx值右移n位(n为cl中的值),相当于除以2的n次方

9)lea指令。地址传送指令,将有效地址传送到指定的寄存器。

lea eax,DWORD PTR_arr$[ebp]

lea指令的作用,是DWORD PTR_arr$[ebp]对应空间的内存地址值放到eax中。

<3>控制流指令:

x86处理器维持着一个指示当前执行指令的指令指针(IP),当一条指令执行后,此指针自动指向向下一条指令。IP寄存器不能直接操作,但可以用控制流指令更新。通常用标签(label)指示程序中的指令地址,在x86汇编代码中,可在任何指令前加入标签。例如:

       mov esi,[ebp+8]
begin: xor ecx,ecx
       mov eax,[esi]

这样就用begin(begin代表标签名,可以为别的名字)指示了第二条指令,控制流指令通过标签可以实现实现指令的跳转。

1)jmp指令。jmp指令控制IP转移到label所指示的地址(从label中取出指令执行)。

语法如下:

jmp <label>

举例:

jmp begin  # 跳转到begin标记的指令执行

2)jcondition指令。条件转移指令,依据CPU状态字中的一系列条件状态转移,CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。

语法如下:

je <label> (jump when equal)
jne <label> (jump when not equal)
jz <label> (jump when last result was zero)
jp <label> (jump when greater than)
jge <label> (jump when greater than or equal to)
jl <label> (jump when less than)
jle <label> (jump when less than or equal to)

举例:

cmp eax,ebx
jle done # 如果eax的值小于等于ebx的值,跳转到done指示的指令执行,否则执行下一跳指令

3)cmp/test指令。cmp指令用于比较两个操作数的值,test指令对两个操作数进行逐位与运算,这两类指令都不保存操作结果,仅根据运算结果设置CPU状态字中的条件码。

语法如下:

cmp <reg>,<reg> / test <reg>,<reg>
cmp <reg>,<mem> / test <reg>,<mem>
cmp <mem>,<reg> / test <mem>,<reg>
cmp <reg>,<con> / test <reg>,<con>

cmp和test指令通常和jcondition指令搭配使用,举例:

cmp dword ptr [var],10 # 将var指示的主存地址的4字节内容,与10比较
jne loop  # 如果相等则继续顺序执行,否则跳转到loop处执行
test eax,eax  # 测试eax是否为零
jz xxxx   # 为零则置标志ZF为1,跳转到xxxx处执行

4)call/ret指令。分别用于实现程序(过程,函数等)的调用及返回。

语法如下:

call <label>
ret

call指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其他简单的跳转指令不同,call指令保存调用之前的地址信息(当call指令结束后,返回调用之前的地址)。

ret指令实现了程序的返回机制,ret指令弹出栈中保存的指令地址,然后无条件转一件到保存的指令地址执行,call和ret是程序(函数)调用中最关键的两条指令。

3. 条件码

编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。

1.条件码(标志位)

除了整数寄存器,CPU还维护者一组条件码(标志位)寄存器,他们描述了最近的算术或逻辑运算操作的属性。可以检测这些寄存器来执行条件分支指令,最常用的条件码有:

CF:进(借)为标志。最近无符号整数加(减)运算后的进(借)位情况。有进(借)位,CF=1,否则CF=0。如(unsigned)t<(unsigned)a,因为判断大小是相减。

ZF:零标志。最近的操作的运算结算是否为0,若结果为0,ZF=1;否则ZF=0。如(t==0)。

SF:符号标志。最近的带符号数运算结果的符号。负数时,SF=1;否则SF=0。

OF:溢出标志。最近带符号数运算的结果是否溢出,若溢出,OF=1;否则OF=0。

可见,OF和SF对无符号数运算来说没有意义,而CF对带符号数来说没有意义

如何判断溢出,简单的就是正数相加变负数为溢出,负数相加变正数溢出。考研中通常考十六进制的两个数的溢出,可如下方法判断:

  • 数据位高位进位,符号位进位未进位,溢出。
  • 数据位高位未进位,符号位进位,溢出。
  • 数据位高位进位,符号位进位,不溢出。
  • 数据位高位未进位,符号位未进位,不溢出。

简单来说就是数据位高位和符号位高位进位不一样的时候就会溢出

常见的算术逻辑运算指令(add,sub,imul,or,and,shl,inc,dec,not,sal等)会设置条件码。但有两类指令只设置条件码而不改变任何其他寄存器,即cmp和test指令,cmp指令和sub指令的行为一样,test指令与and指令的行为一样,但它们只设置条件码,而不更新目的寄存器

控制流指令中的jcondition条件转移指令,就是根据条件码ZF和SF来实现跳转

注意:乘法溢出后,可以跳转到“溢出自陷指令”,例如int 0x2e就是一条自陷指令,但是考研只需要掌握溢出,可以跳转到“溢出自陷指令”即可,不需要记自陷指令有哪些。

第三节:各种变量赋值汇编实战

1. 各种变量赋值汇编实战解析

针对整型,整型数组,整型指针变量的赋值(浮点与字符等价的),对应的汇编进行解析。首先编写C代码。

#include <stdio.h>

int main() {
    int arr[3]={1,2,3};
    int *p;
    int i=5;
    int j=10;
    i=arr[2];
    p=arr;
    printf("i=%d\n",i);
    return 0;
}
# 生产汇编代码指令
gcc -m32 -masm=intel -S -fverbose-asm main.c

C代码在让CPU去运行时,其实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程

我们访问所有变量的空间都是通过栈指针(esp时刻都存着栈指针,也可以称为栈顶指针)的偏移,来获取对应变量内存空间的数据的

	.text
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "i=%d\12\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	push	ebp	 #
	mov	ebp, esp	 #,
	and	esp, -16	 #,
	sub	esp, 48	 #,
 # main.c:3: int main() {
	call	___main	 #
 # main.c:4:     int arr[3]={1,2,3};
	mov	DWORD PTR [esp+24], 1	 # arr, # 把常量1放入栈指针(esp寄存器存的栈指针)偏移24个字节的位置
	mov	DWORD PTR [esp+28], 2	 # arr, # 同上解释
	mov	DWORD PTR [esp+32], 3	 # arr, # 同上解释
 # main.c:6:     int i=5;
	mov	DWORD PTR [esp+44], 5	 # i, # 把常量5放入栈指针偏移44个字节的位置,这个位置时变量i的空间
 # main.c:7:     int j=10;
	mov	DWORD PTR [esp+40], 10	 # j, # 把常量10放入栈指针偏移40个字节的位置,这个位置时变量j的空间
 # main.c:8:     i=arr[2];
	mov	eax, DWORD PTR [esp+32]	 # tmp89, arr # 把栈指针偏移32个字节的位置拿到的数据,存放到寄存器eax中
	mov	DWORD PTR [esp+44], eax	 # i, tmp89 # 把eax寄存器的内容放入栈指针偏移44个字节的位置,偏移44个字节的位置刚好是i的空间
 # main.c:9:     p=arr;
	lea	eax, [esp+24]	 # tmp90,  # lea和mov不一样,是拿栈指针偏移24个字节的位置的地址,把地址放到eax寄存器中
	mov	DWORD PTR [esp+36], eax	 # p, tmp90 # 把eax寄存器的内容放入栈指针偏移36个字节的位置。栈指针偏移36个字节的位置,刚好是p的空间
 # main.c:10:     printf("i=%d\n",i);
	mov	eax, DWORD PTR [esp+44]	 # tmp91, i # 把栈指针,偏移44个字节的位置拿到的数据,放到寄存器eax中
	mov	DWORD PTR [esp+4], eax	 #, tmp91 # 把eax数据放到栈指针偏移4个字节位置的内存中
	mov	DWORD PTR [esp], OFFSET FLAT:LC0	 #, # 把LC0(也就是上面那个字符串)的地址,放到寄存器栈指针执行的内位置
	call	_printf	 # # 调用printf函数
 # main.c:11:     return 0;
	mov	eax, 0	 # _10,
 # main.c:12: }
	leave	
	ret	
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	_printf;	.scl	2;	.type	32;	.endef

说明:ptr是pointer(指针)的缩写。

汇编里面ptr是规定的字(既保留字),是用来临时指定类型的。可以理解为:ptr是临时的类型转换,相当于C语言中的强制类型转换。

例如:mov ax,bx。是把bx寄存器里的值赋予ax,由于二者都是寄存器,长度已定(word型),所以没有必要加WORD.

mov ax,word ptr [bx]。是把内存地址等于“bx寄存器的值”的地方所存放的数据,赋予ax。由于只是给出一个内存地址,不知道希望赋予的ax的,是byte还是word,所以可以用word明确指出;如果不用,既(mov ax,[bx]),则在8086中是默认传递一个字,即链各个字节给ax。

intel中的:

  • dword ptr 长字(4字节)
  • word ptr 双字
  • byte ptr 一字节

第四节:选择循环汇编实战

1. 选择循环汇编实战解析

#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分别是我们要用到的两个字符串常量的起始地址
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "i is small\0"
LC1:
	.ascii "this is loop\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	push	ebp	 #
	mov	ebp, esp	 #,
	and	esp, -16	 #,
	sub	esp, 32	 #,
 # main.c:3: int main() {
	call	___main	 #
 # main.c:4:     int i=5;
	mov	DWORD PTR [esp+28], 5	 # i, # 把常量5放入栈指针偏移28个字节的位置,这个位置是变量i的空间
 # main.c:5:     int j=10;
	mov	DWORD PTR [esp+24], 10	 # j, # 把常量10放入栈指针偏移24个字节的位置,这个位置是变量j的空间
 # main.c:6:     if(i<j){
	mov	eax, DWORD PTR [esp+28]	 # tmp89, i # 把栈指针偏移28个字节的位置内的值,放入eax寄存器
	cmp	eax, DWORD PTR [esp+24]	 # tmp89, j # 比较eax寄存器内的值和栈指针偏移24个字节位置的值的大小,拿eax寄存器的值减去DWORD PTR [esp+24],然后设置条件码
	jge	L2	 #, # 如果eax寄存器大于等于DWORD PTR [esp+24],那么跳转到L2,否则直接往下执行,jge是根据条件码ZF和SF来判断的。
 # main.c:7:         printf("i is small\n");
	mov	DWORD PTR [esp], OFFSET FLAT:LC0	 #, # 把LC0的地址,放到寄存器栈指针指向的内存位置
	call	_puts	 #
L2:
 # main.c:9:     for(i=0;i<5;i++){
	mov	DWORD PTR [esp+28], 0	 # i, # 把常量0放入栈指针偏移28个字节的位置,这个位置是变量i的空间
 # main.c:9:     for(i=0;i<5;i++){
	jmp	L3	 # # 无条件跳转到L3
L4:
 # main.c:10:         printf("this is loop\n");
	mov	DWORD PTR [esp], OFFSET FLAT:LC1	 #, # 把LC1的地址,放到寄存器栈指针指向的内存位置
	call	_puts	 #
 # main.c:9:     for(i=0;i<5;i++){
	add	DWORD PTR [esp+28], 1	 # i,
L3:
 # main.c:9:     for(i=0;i<5;i++){
	cmp	DWORD PTR [esp+28], 4	 # i, # 比较栈指针偏移24个字节位置的值域4的大小,拿DWORD PTR [esp+24]减去4,然后形成条件码
	jle	L4	 #, # 小于等于就跳转到L4
 # main.c:12:     return 0;
	mov	eax, 0	 # _11,
 # main.c:13: }
	leave	
	ret	
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	_puts;	.scl	2;	.type	32;	.endef

这里主要掌握指令:cmp,jge,jmp,jle等,以及了解以下字符串常量是存放在文字常量区。

第五节:函数调用汇编

1. 函数调用汇编实战

#include <stdio.h>

int add(int a,int b){
    int ret;
    ret=a+b;
    return ret;
}
int main() {
    int a,b,ret;
    int *p;
    a=5;
    p=&a;
    b=*p+2;
    ret=add(a,b);
    printf("add result=%d\n",ret);
    return 0;
}

生成汇编代码的方法和上一节一致。

函数调用的汇编原理解析:

首先明确一点,函数栈是向下生长的,所谓向下生长,是指从内存高地址向内存低地址的路径延伸。于是,栈就有了栈底和栈顶,栈顶的地址要不栈底的低。

对x86体系的CPU而言,寄存器ebp可称为帧指针或基址指针(base poin
ter),寄存器esp可以称为栈指针(stack pointer)。

说明:

  1. ebp在为改变之前始终指向栈帧的开始(也就是栈底),所以ebp的用途是在堆栈中寻址。

  2. esp会随着数据的入栈和出栈而移动,即esp始终指向栈顶。

假设函数A调用函数B,称函数A为调用者,函数B为被调用者,则函数调用过程描述如下:

(1)首先将调用者A的堆栈的基址(ebp)入栈,以保存之前任务的信息。

(2)然后将调用者A的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底),原有函数的栈顶,是新函数的栈底。

(3)再后在这个基址(被调用者B的栈底)上开辟(一般是sub指令)相应的空间用作被调用者B的栈空间。

(4)函数B返回后,当前栈帧的ebp恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置,然后调用者A从恢复后的栈顶弹出之前的ebp值(因为这个值在函数调用前一步被压入堆栈)。

这样,ebp和esp就都回复了调用函数B前的位置,即栈恢复函数B调用前的状态,相当于(ret指令做了什么)

mov esp,ebp  // 把ebp内的内容赋值到esp寄存器中,也就是B函数的栈基作为原有调用者A函数的栈顶
pop ebp  // 弹出栈顶元素,放到ebp寄存器中,因为原有A函数的栈基指针被压到了内存里,所以弹出后,放入ebp,这样原函数A的现场恢复完毕

	.text
	.globl	_add
	.def	_add;	.scl	2;	.type	32;	.endef
_add: # add函数的入口,这里阅读需要结合函数调用图
	push	ebp	 #  # 把原有函数,也就是main函数的栈基指针压栈,压栈是把ebp的值保存到内存上,位置就是esp指向的位置
	mov	ebp, esp	 #,  # 把main的栈顶指针esp,作为add函数的栈基指针ebp
	sub	esp, 16	 #,  # 由于add函数自身要使用栈空间,把esp减去16,是指add函数的栈空间大小是16个字节
 # main.c:5:     ret=a+b;
	mov	edx, DWORD PTR [ebp+8]	 # tmp93, a # 拿到实参,也就是a的值,放入edx
	mov	eax, DWORD PTR [ebp+12]	 # tmp94, b # 拿到实参,也就是b的值,放入eax
	add	eax, edx	 # tmp92, tmp93 # jiang eax和edx相加
	mov	DWORD PTR [ebp-4], eax	 # ret, tmp92  # 把eax,也就是ret的值,放入ebp减4个字节位置
 # main.c:6:     return ret;
	mov	eax, DWORD PTR [ebp-4]	 # _4, ret
 # main.c:7: }
	leave	
	ret	  # 函数返回,弹出压栈的指令返回地址,回到main函数执行
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "add result=%d\12\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	push	ebp	 #
	mov	ebp, esp	 #,
	and	esp, -16	 #,
	sub	esp, 32	 #,
 # main.c:8: int main() {
	call	___main	 #
 # main.c:11:     a=5;
	mov	DWORD PTR [esp+16], 5	 # a, # 把常量5放入栈指针偏移16个字节的位置,这个位置是变量a的空间
 # main.c:12:     p=&a;
	lea	eax, [esp+16]	 # tmp91, # 这里用lea,和mov不一样,是将esp+16位置的地址,放到eax寄存器中
	mov	DWORD PTR [esp+28], eax	 # p, tmp91  # 把eax中的值放到栈指针偏移28字节位置,也就是指针变量p中
 # main.c:13:     b=*p+2;  # 下面两个mov是间接访问的经典解析
	mov	eax, DWORD PTR [esp+28]	 # tmp92, p  # 栈指针偏移28字节位置,也就是指针变量p的值,放到eax寄存器
	mov	eax, DWORD PTR [eax]	 # _1, *p_5  # 把eax寄存器中的值作为地址,去内存访问到相应的数据,放入eax中
 # main.c:13:     b=*p+2;
	add	eax, 2	 # tmp93, # 对eax中的值加2结果,结果还是在eax中
	mov	DWORD PTR [esp+24], eax	 # b, tmp93 # 把eax中的值放到栈指针偏移24字节位置,也就是变量b中
 # main.c:14:     ret=add(a,b);  # 下面是函数调用,实参传递的经典动作,从而理解值传递是怎么实现的
	mov	eax, DWORD PTR [esp+16]	 # a.0_2, a # 栈指针偏移16字节位置,也就是变量a的值,放到eax寄存器
	mov	edx, DWORD PTR [esp+24]	 # tmp94, b # 栈指针偏移24字节位置,也就是变量b的值,放到eax寄存器
	mov	DWORD PTR [esp+4], edx	 #, tmp94 # 把edx中的值(变量b),放到寄存器指针偏移4,自己的内存位置
	mov	DWORD PTR [esp], eax	 #, a.0_2 # 把eax中的值(变量a),放到寄存器栈指针指向的内存位置
	call	_add	 # # 调用add函数
	mov	DWORD PTR [esp+20], eax	 # ret, tmp95
 # main.c:15:     printf("add result=%d\n",ret);
	mov	eax, DWORD PTR [esp+20]	 # tmp96, ret
	mov	DWORD PTR [esp+4], eax	 #, tmp96
	mov	DWORD PTR [esp], OFFSET FLAT:LC0	 #,
	call	_printf	 #
 # main.c:16:     return 0;
	mov	eax, 0	 # _10,
 # main.c:17: }
	leave	
	ret	
	.ident	"GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
	.def	_printf;	.scl	2;	.type	32;	.endef

这里主要掌握add,sub,call,ret等指令。

2. 机器码

如何得到机器码,需要执行下面两条指令。

# 第一条
gcc -m32 -g -o main main.c  (Mac一致)
# 第二条
objdump --source main.exe ->main.dump (Mac去掉.exe后缀,写为main即可)

// 原文很长,我们找到add函数和main函数部分即可
#include <stdio.h>

int add(int a,int b){
  40150f:	90                   	nop

00401510 <_add>:
  401510:	55                   	push   %ebp
  401511:	89 e5                	mov    %esp,%ebp
  401513:	83 ec 10             	sub    $0x10,%esp
    int ret;
    ret=a+b;
  401516:	8b 55 08             	mov    0x8(%ebp),%edx
  401519:	8b 45 0c             	mov    0xc(%ebp),%eax
  40151c:	01 d0                	add    %edx,%eax
  40151e:	89 45 fc             	mov    %eax,-0x4(%ebp)
    return ret;
  401521:	8b 45 fc             	mov    -0x4(%ebp),%eax
}
  401524:	c9                   	leave  
  401525:	c3                   	ret    

00401526 <_main>:
int main() {
  401526:	55                   	push   %ebp
  401527:	89 e5                	mov    %esp,%ebp
  401529:	83 e4 f0             	and    $0xfffffff0,%esp
  40152c:	83 ec 20             	sub    $0x20,%esp
  40152f:	e8 ec 00 00 00       	call   401620 <___main>
    int a,b,ret;
    int *p;
    a=5;
  401534:	c7 44 24 10 05 00 00 	movl   $0x5,0x10(%esp)
  40153b:	00 
    p=&a;
  40153c:	8d 44 24 10          	lea    0x10(%esp),%eax
  401540:	89 44 24 1c          	mov    %eax,0x1c(%esp)
    b=*p+2;
  401544:	8b 44 24 1c          	mov    0x1c(%esp),%eax
  401548:	8b 00                	mov    (%eax),%eax
  40154a:	83 c0 02             	add    $0x2,%eax
  40154d:	89 44 24 18          	mov    %eax,0x18(%esp)
    ret=add(a,b);
  401551:	8b 44 24 10          	mov    0x10(%esp),%eax
  401555:	8b 54 24 18          	mov    0x18(%esp),%edx
  401559:	89 54 24 04          	mov    %edx,0x4(%esp)
  40155d:	89 04 24             	mov    %eax,(%esp)
  401560:	e8 ab ff ff ff       	call   401510 <_add> // 掌握这里即可
  401565:	89 44 24 14          	mov    %eax,0x14(%esp)
    printf("add result=%d\n",ret);
  401569:	8b 44 24 14          	mov    0x14(%esp),%eax
  40156d:	89 44 24 04          	mov    %eax,0x4(%esp)
  401571:	c7 04 24 00 40 40 00 	movl   $0x404000,(%esp)
  401578:	e8 bf 0f 00 00       	call   40253c <_printf>
    return 0;
  40157d:	b8 00 00 00 00       	mov    $0x0,%eax
}

说明:对于机器码,我们只需要掌握e8 ab ff ff ff call 401510 <_add>中的e8 ab ff ff ff是什么含义即可,e8代表call,而ab ff ff ff是通过00401510-401565所得。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

傻啦猫@_@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值