标签:
第10章通过汇编语言了解程序的实际构成
热身问题
1.本地代码指令中,表示其功能的英文缩写称为什么?
助记符、汇编语言是通过利用助记符来记述程序的。
2.汇编语言的源代码转换成本地代码的方式称为什么?
汇编、使用汇编器这个工具来进行汇编。
3.本地代码转换成汇编语言的源代码的方式称为什么?
反汇编、通过返汇编,得到人们可以理解的代码。
4.汇编语言的源文件的扩展名,通常是什么格式?
.asm、.asm是assembler(汇编器)的简称
5.汇编语言程序中的段定义指的是什么?
构成程序的命令和数据的集合组、在高级语言的源代码中,即使指令和数据在编写时是分散的,编译后也会在段定义中集合汇总起来。
6.汇编语言的跳转指令,是在何种情况下使用的?
将程序流程跳转到其它地址时需要用到该指令、在汇编语言中,通过跳转指令,可以实现循环和条件分支。
10.1 汇编语言和本地代码是一一对应的
计算机CPU能直接解释运行的只有本地代码(机器语言)程序。用C语言等编写的源代码,需要通过各自的编译器编译后,转换成本地代码。
通过汇编语言编写的源代码,最终也必须要转换成本地代码才能运行。负责转换工作的程序称为汇编器,转换这一处理本身称为汇编。
用汇编语言编写的源代码,和本地代码是一一对应的。因而,本地代码也可以反过来转换成汇编语言的源代码。持有该功能的逆变换程序称为反汇编程序,逆变换这一处理本身称为反汇编。
10.2 通过编译器输出汇编语言的源代码
大部分C语言编译器,都可以把利用C语言编写的源代码转换成汇编语言的源代码,而不是本地代码。利用该功能就可以得到汇编语言的源代码。
main函数是程序运行的起始位置,程序运行的起始位置也称为“入口点”。
汇编语言的源代码,是由转换操作码的指令和针对汇编器的伪指令构成的。
伪指令负责把程序的构造以及汇编的方法指示给汇编器。不过伪指令本身是无法汇编转换成本地代码的。
由伪指令segment和ends围起来的部分,是给构成程序的命令和数据的集合加上一个名字而得到的,称为段定义。段定义的英文表达segment具有“区域”的意思。在程序中,段定义指的是命令和数据等程序的集合体的意思。一个程序有多个段定义构成。
汇编语言指令的语法结构是操作码+操作数(也存在着只有操作码没有操作数的指令),操作码(opcode)表示指令动作,操作数表示指令对象。
本地代码加载到内存后才能运行。内存中存储着构成本地代码的指令和数据。程序运行时,CPU会从内存中把指令和数据读出,然后再将其存储在CPU内部的寄存器中进行处理。
程序运行时,会在内存申请分配一个称为栈的数据空间。栈(stack)有“干草堆积如山”的意思。就如该名称所表示的那样,数据在存储时是从内存的下层(大的地址编号)逐渐往上层(小的地址编号)累积,读出则是按照从上往下的顺序进行。
32位系列的CPU中,进行1次push或pop,即可处理32位(4字节)的数据。
push指令和pop指令运行后,esp寄存器的值会自动进行更新(push指令是-4,pop指令是+4),因而程序猿没有必要指定内存地址里。
以下面的程序为例:
int AddNum(int a,int b)
{
return a+b;
}
void MyFunc()
{
int c;
c= AddNum(123,456);
}
转换为汇编语言如下:
_MyFunc proc near
push ebp ; 将ebp(扩展基址指针寄存器)的值存入栈中 (1)
mov ebp,esp ; 将esp(扩展栈指针寄存器)的值存入ebp寄存器 (2)
push 456 ; 456入栈 (3)
push 123 ; 123入栈 (4)
call _AddNum ; 调用AddNum函数 (5)
add esp,8 ; esp寄存器的值加8 (6)
pop ebp ; 读出栈中的值,此处存入到esp寄存器 (7)
ret ; 结束MyFunc函数,返回到调用源 (8)
_MyFunc endp
其中:
ebp(extended base pointer):扩展基址指针寄存器:存储数据存储领域基点的内存地址。
esp(Extended stack pointer):扩展栈指针寄存器 :存储栈中最高位数据的内存地址。对栈进行读写的内存地址是由esp寄存器(栈指针)进行管理的。
eax:数据寄存器中的累加器(accumulator)。
e是extend的意思,为区别于16位CPU的寄存器。
mov A,B 把B的值赋值给我
mov eax,dword ptr [ebp+8] //[ebp+8]表示ebp+8所指向的内存,dword ptr表示double word pointer,以为从ebp+8所指向的内存读取4个字节的数据写入到eax寄存器。
and A,B 把A同B的值相加,并将结果赋值给A
call A 调用函数A
ret 无 将处理返回到函数的调用源
push A 把A的值存储到栈中
pop A 从栈中读出值,并将其赋值给A
(3)和(4)表示的是传递给AddNum函数的参数通过push入栈。虽然调用时是AddNum(123,456),但456会先入栈。
(5)在汇编语言中,函数名表示的是函数所在的内存地址。
(6)此处时调用完AddNum指令后要返回的地址,通过此操作可以把栈中的两个参数(456和123)进行销毁处理,也就是栈清理处理。
(6)虽然通过两次pop指令也可以实现,不过采用esp加8的方式更加有效率。
(6)虽然内存中的数据实际上还残留着,但只要把esp寄存器的值更新为数据存储地址前面的数据位置,该数据就相当于被销毁了。
_AddNum proc near
push ebp ——————————(1)
mov ebp,esp ————————(2)
mov eax,dword ptr [ebp+8] ——(3)//[ebp+8]表示ebp+8所指向的内存,
add eax,dword ptr [ebp+12] —-(4)
pop ebp ——————————(5)
ret ———————————-(6)
_AddNum endup
(1)+(5):注意ebp的值在(1)中被压入栈,在(5)中从栈中读出,重新写入ebp(扩展基址指针寄存器),这是因为寄存器的数量是有限的,为了在函数内使用该寄存器,所以在函数进入时保存ebp的值,在函数返回时再恢复它的值。
(2)中把负责管理栈地址的esp寄存器的值赋值到了ebp寄存器中。这是因为,在mov指令中方括号内的参数,是不允许指定esp寄存器的。因此,这里就采用了不直接通过esp,而是通过ebp寄存器来读写栈内容的方法。
(3)使用[ebp+8]指定栈中存储的第1个参数123,并将其读出到eax寄存器中。像这样,不使用pop指令,也可以参照栈的内容。而之所以从多个寄存器中选中了eax寄存器,是因为eas寄存器是负责运算的累加寄存器。
(4):通过(4)的add指令,把当前eax寄存器的值同第二个参数相加后的结果存储到eax寄存器中。[ebp+12]是用来指定第2个参数456的。在C语言中,函数的返回值必须通过eax寄存器返回,这也是规定。不过,和ebp寄存器不同的是,eax寄存器的值不用还原到原始状态。
(6)中ret指令运行后,函数返回目的地的内存地址会自动出栈,程序流程就会跳转返回到call _AddNum指令的下一条地址。
至此,我们进行了很多解释,就是为了说明“函数的参数是通过栈来传递,返回值是通过寄存器来返回的”这一点。
上面两个段代码结合起来,栈的情况如下:空白表示未使用的空间
AddNum函数调用前 |
函数的入口 |
运算处理时 |
函数的出口 |
从AddNum返回后 |
MyFunc函数处理完毕时 |
|
|
|
|
|
|
|
|
ebp |
|
|
|
返回目的地的内存地址 |
返回目的地的内存地址 |
返回目的地的内存地址 |
|
|
|
123 |
123 |
123 |
123 |
123 |
|
456 |
456 |
456 |
456 |
456 |
|
ebp寄存器的值 |
ebp寄存器的值 |
ebp寄存器的值 |
ebp寄存器的值 |
ebp寄存器的值 |
|
. |
. |
. |
. |
. |
. |
10.9 始终确保全局变量的内存空间。
C语言中在函数外部定义的变量称为全局变量,在函数内部定义的变量称为局部变量。全局变量可以参阅源代码的任意部分,而局部变量只能在定义该变量的函数内部进行参阅。
编译后的程序,会被归类到名为段定义的组中,以Borland C++为例,转换成汇编后,初始化的全局变量,会被汇总到名为DATA的段定义中,没有被初始化的全局变量,会被汇总到BSS的段定义中。指令则被汇总到名为TEXT的段定义中。
以全局变量int a= 1;为例
DATA segment
_a label dword
dd 1
DATA ends
_a label dword 定义了_a这个标签。标签表示的是相对于段定义起始位置的位置。由于_a在DATA段定义的开始位置,所以相对位置是0。_a就相当于全局变量a。
dd 1指的是,申请分配了4个字节的内存空间,存储着1这个初始值。dd(define double word)表示的是两个长度为2的字节领域(word),也就是4字节的意思。
以 int b;为例
BSS segment
b label dword
db 4 dup(?)
BSS ends
db 4 dup(?)表示申请分配了4字节的领域,但之尚未确定。db表示(define byte)有1个长度是1字节的内存空间。
Borland C++中,之所以把全局变量分为已经初始化的全局变量和没有初始化的全局变量,是因为,程序运行时没有初始化的全局变量的领域都会被设定为0进行初始化。可见,通过汇总,初始化很容易实现,只是把内存的特定范围设定为0就可以了。
10.10 临时确保局部变量用的内存空间
局部变量被临时保存在寄存器和栈中。函数内部利用的栈,在函数处理完毕后会恢复到初始状态,因此局部变量的值也就被销毁了,局部变量只在函数处理运行期间临时存储在寄存器和栈上。
Borland C++编译器自动优化有可能把局部变量分配到寄存器中,寄存器空间时使用寄存器,寄存器空间不足的话就使用栈。
10.11 循环处理的实现方法
以下面代码为例
void MySub()
{
//不做任何处理
}
Void MyFunc()
{
int i;
for (i = 0;i < 10; i++)//其中i称为循环计数器
{
//重复调用MySub函数10次
MySub();
}
}
将代码中的for循环语句转换成汇编语言后:
其中ebx为基址寄存器,存储内存地址。
xor A,B A和B进行异或比较,并将结果存入A中
inc A A的值加1
cmp A,B 对A和B的值进行比较,比较结果会自动存入标志寄存器中
jl 标签 和cmp命令组合使用。跳转到标签行
xor ebx,ebx ; 将eax寄存器清0-------1
@4 call _MySub ; 调用MySub函数-------2
inc ebx ; ebx寄存器的值加1------3
cmp ebx,10 ; 将ebx寄存器的值和10进行比较-4
jl short @4 ; 如果小于10 就跳转到@4 ---5
1:MyFunc函数中用到的局部变量只有i,变量i申请分配了ebx寄存器的内存空间。for语句的括号中的i=0;被转换成了xor ebx,ebx这一处理。不管ebx当时的值是什么,结果肯定是0。虽然用mov ebx,0也会得到同样的结果,但与mov指令相比,xor指令的处理速度更快。
4:cmp ebx,10就相当于i<10这个处理,把比较结果存储到标志寄存器中。
5:标志寄存器的值,程序时无法直接参考的。那么程序如何判断比较结果呢?实际上,汇编语言中有多个跳转指令,这些跳转指令会根据标志寄存器的值来判断是否需要跳转。jl是jump on less than(小于的话就跳转)的意思。5的意思是,ebx若小于10的话就跳转到@4标签。
人们常说“汇编语言是对CPU的实际运行进行直接描述的低级编程语言,C语言是用与人类的感觉相近的表现来描述的高级编程语言”,如果用C语言表示上述的汇编代码,则有:
i^=i;
L4: MySub();
i++;
if(i<20) goto L4;
10.12 条件分支的实现方法
以以下函数为例:
void MySub1()
{
//不做任何事情
}
void MySub2()
{
//不做任何事情
}
void MySub3()
{
//不做任何事情
}
void MyFunc()
{
int a = 123;
if ( a>100 )
{
MySub1();
}
else if (a<50)
{
MySub2();
}
else
{
MySub3();
}
}
转换成汇编后:
_MuFunc proc near
push ebp ;
mov ebp,esp ;
mov eax,123 ; 把123存入eax寄存器中
cmp eax,100 ; 把eax寄存器的值同100进行比较
jle short @8 ; 比100小时,跳转到@8标签
call _MySub1 ;
jmp short @11 ; 跳转到@11标签
@8: cmp eax,50 ; 把eax寄存器的值同50进行比较
jge short @10 ; 大于50时,跳转到@10
call _MySun2 ;
jmp short @11 ;
@10: call _MySub3
@11: pop ebp
ret
_MyFunc endp
上述代码清单中用到了三种跳转指令,分别是比较结果小时跳转的jle(jump on less or equal),大时跳转到jge(jump on greater or equal)、无论结果如何都无条件跳转的jmp。此处代码使用了eax寄存器存储变量a。
虽然大部分的C语言参考书中都写着“为了便于理解程序的结构,应尽量避免使用无条件分支的goto语句”,不过在汇编语言这一领域中,如果不使用相当于C语言的goto语句的jmp指令,就无法实现循环和条件分支。由此看来,关于应不应该在C语言中使用goto语句,大家没有必要这么紧张。
程序是怎样跑起来的-第10章 通过汇编语言了解程序的实际构成
标签:
原文地址:http://blog.csdn.net/u014222687/article/details/51509726