通过汇编语言了解程序的实际构成
问题
-
本地代码的指令中,表示其功能的英语缩写称为什么?
助记符
-
汇编语言的源代码转换成本地代码的方式称为什么?
汇编
-
本地代码转换成汇编语言的源代码的方式称为什么?
反汇编
-
汇编语言的源文件的扩展名,通常是什么格式?
.asm
-
汇编语言程序中的段定义指的是什么?
构成程序的命令和数据的集合组
-
汇编语言的跳转指令,是在何种情况下使用的?
将程序流程跳转到其他地址时需要用到跳转指令。在汇编语言中,跳转指令可以实现循环和条件分支。
汇编语言和本地代码是一一对应的
表示其功能的英语缩写单词称为助记符,使用助记符的编程语言成为汇编语言。汇编语言编写的源代码最终也要转换成本地代码才能运行,负责转换工作的程序称为汇编器,转换这一处理本身称为汇编。用汇编语言编写的源代码和本地代码是一一对应的。因而,本地代码也可以反过来转换成汇编语言的源代码,这一过程称为反汇编,持有该功能的逆变换程序称为反汇编程序。
不过,本地代码变换成高级语言源代码的反编译,则要比反汇编要困难许多,几乎是不太可能的,因为高级语言的源代码和本地代码不是一一对应的。
通过编译器输出汇编语言的源代码
大部分C语言编译器,都可以把利用C语言编写的源代码转换成汇编语言的源代码,而不是本地代码。利用该功能,就可以对C语言的源代码和汇编语言的源代码进行比较研究。
汇编语言源文件的扩展名通常用.asm来表示。
不会转换成本地代码的伪指令
汇编语言的源代码,是由转换成本地代码的指令和针对汇编器的伪指令构成的。伪指令负责把程序的构造即汇编的方法只是给汇编器。不过伪指令本身是无法汇编转换成本地代码的。
由伪指令segment和ends围起来的部分,是给构成程序的命令和数据的集合体加上一个名字而得到的,称为段定义。一个程序由多个段定义构成。
group这一伪指令,表示的是把多个段定义汇总成一个新名字的组。
伪指令proc和endp围起来的部分,表示的是过程procedure的范围。在汇编语言中,过程相当于C语言的函数的形式。
伪指令end表示的是源代码的结束。
汇编语言的语法是操作码+操作数
也存在只有操作码,没有操作数的指令
操作码表示的是指令动作,操作数表示的是指令对象。
存在多个操作数时,用逗号分隔。
能够使用何种形式的操作码,是由CPU的种类决定的。
常用的操作码功能:
操作码 | 操作数 | 功能 |
---|---|---|
mov | A, B | 把B的值赋给A |
and | A, B | 把A同B的值相加,并将结果赋给A |
push | A | 把A的值存储在栈中 |
pop | A | 从栈中读取出值,并将其赋给A |
call | A | 调用函数A |
ret | 无 | 将处理返回到函数的调用源 |
寄存器是CPU中的存储区域。不过,寄存器不仅仅具有存储指令和数据的功能,也有运算功能。
主要寄存器:
寄存器名 | 名称 | 主要功能 |
---|---|---|
eax | 累加寄存器 | 运算 |
ebx | 基址寄存器 | 存储内存地址 |
ecx | 计数寄存器 | 计算循环次数 |
edx | 数据计数器 | 存储数据 |
esi | 源基址寄存器 | 存储数据发送源的内存地址 |
edi | 目标基址寄存器 | 存储数据发送目标的内存地址 |
ebp | 扩展基址指针寄存器 | 存储数据存储领域基点的内存地址 |
esp | 扩展栈指针寄存器 | 存储栈中最高位数据的内存地址 |
最常用的mov指令
操作数可以指定寄存器、常数、标签,以及方括号围起来的内容。如果指令了没有用方括号围起来的内容,就表示对该值进行处理;如果指令了用方括号围起来的内容,方括号中的值则会被解释为内存地址,然后就会对该内存地址对应的值进行读写操作。
dword ptr(double word pointer)表示的是从指定内存地址读出4字节的数据。
对栈进行push和pop
栈是存储临时数据的区域,它的特点是通过push指令和pop指令进行数据的存储和读出,称为入栈和出栈。push和pop指令都只有一个操作数,这是因为,对栈进行读写的内存地址是由esp寄存器进行管理的。我是怕寄存器的值会自动进行更新,因而程序员就没有必要指定内存地址了。
函数调用机制
形参中位于后面的数值先入栈,这是C语言的规定。
在汇编语言中,函数名表示的是函数所在的内存地址。
虽然通过两次pop指令也可以实现湛清里,不过采用esp寄存器加8的方式会更有效率。
push和pop必须以4字节为单位对数据进行入栈和出栈处理,长度小于4字节的数据也占用了4字节的栈区域。
编译器有最优化功能。最优化功能是编译器在本地代码上费劲功夫实现的,其目的是让编译后的程序运行速度更快、文件更小。
函数内部的处理
CPU拥有的寄存器是由数量限制的。在函数调用前,调用源有可能已经在使用寄存器了。因而,在函数内部利用的寄存器,要尽量返回到函数调用前的状态。为此,我们就需要将其暂时保存在栈中,然后 再在函数处理完毕之前出栈,使其返回到原来的状态。
在C语言中,函数的返回值必须通过eax寄存器返回,这也是规定。不过,eax寄存器无需还原到原始状态。
函数的参数是通过栈来传递的,返回值是通过寄存器来返回的。
始终确保全局变量用的内存空间
C语言中,在函数外部定义的变量称为全局变量,在函数内部定义的变量称为局部变量。
初始化的全局变量会被汇总到名为_DATA的段定义中;没有初始化的全局变量会被汇总到名为_BSS的段定义中;指令会被汇总到名为_TEXT的段定义中。
标签表示的是相对于段定义起始位置的位置。编译后的函数名和变量名会附加一个下划线,这也是Borland C++的规定。
dd(define double word)表示的是由两个长度为2的字节领域(word),也就是4字节的意思。
db(define byte)表示有1个长度是1字节的内存空间,db 4 dup(?)就是4字节的内存空间,但值尚未确定。
临时确保局部变量用的内存空间
局部案例是临时保存在寄存器和栈中的。函数内部利用的栈,在函数处理完毕后会恢复到初始状态,因此局部变量的值也就被销毁了,而寄存器也可能会被用于其他目的。
寄存器先时就使用寄存器,寄存器空间不足时就使用栈。局部变量利用寄存器,是Borland C++编译器最优化的运行结果。旧的编译器没有类似的最优化功能,局部变量就可能会仅仅使用栈。至于使用哪个寄存器则要由编译器来决定。这种情况下,寄存器只是被单纯地用于存储变量的值,和其本身的角色没有任何关系。
循环处理、条件分支的实现方法
C语言的for语句是通过在括号中指定循环计数器的初始值、循环的继续条件、循环计数器的更新来进行循环处理的。与此相对,在汇编语言的源代码汇总,循环是通过比较指令cmp和跳转指令jl来实现的。
与mov指令相比,xor指令的处理速度更快。
汇编语言中比较指令的结果,会存储在CPU的标志寄存器中。不过,标志寄存器的值,程序是无法直接参考的。实际上,汇编语言有多个跳转指令,这些跳转指令会根据标志寄存器的值来判定是否需要跳转。
虽然大部分的C语言参考书中都写着“为了便于理解程序的结构,应尽量避免使用无条件分支的goto语句”,不过,在汇编语言这一领域中,如果不使用相当于goto语句的jmp指令,就无法实现循环和条件分支。
了解程序运行方式的必要性
多线程处理的Bug,如果没有调查过汇编语言的源代码,即对程序的实际运行方式不了解的话,是很难找到其原因的。
为了避免这种Bug,我们可以采用以函数或C语言源代码的行为单位来禁止线程切换的锁定方法。通过锁定,在特定范围内的处理完成之前,处理不会被切换到其他函数中。
现在基本上没有人用汇编语言来编写程序了。因为C语言等高级编程语言用1行就可以完成的事,汇编语言需要多行,效率很低。不过,汇编语言的经验还是很重要的。因为借助汇编语言,我们可以更好地了解计算机的机制。特别是对专业程序员来说,至少要有一次使用汇编语言的经验。