通过汇编语言了解程序的实际构成

问题

  1. 本地代码的指令中,表示其功能的英语缩写称为什么?

    助记符

  2. 汇编语言的源代码转换成本地代码的方式称为什么?

    汇编

  3. 本地代码转换成汇编语言的源代码的方式称为什么?

    反汇编

  4. 汇编语言的源文件的扩展名,通常是什么格式?

    .asm

  5. 汇编语言程序中的段定义指的是什么?

    构成程序的命令和数据的集合组

  6. 汇编语言的跳转指令,是在何种情况下使用的?

    将程序流程跳转到其他地址时需要用到跳转指令。在汇编语言中,跳转指令可以实现循环和条件分支。

汇编语言和本地代码是一一对应的

表示其功能的英语缩写单词称为助记符,使用助记符的编程语言成为汇编语言。汇编语言编写的源代码最终也要转换成本地代码才能运行,负责转换工作的程序称为汇编器,转换这一处理本身称为汇编。用汇编语言编写的源代码和本地代码是一一对应的。因而,本地代码也可以反过来转换成汇编语言的源代码,这一过程称为反汇编,持有该功能的逆变换程序称为反汇编程序。

不过,本地代码变换成高级语言源代码的反编译,则要比反汇编要困难许多,几乎是不太可能的,因为高级语言的源代码和本地代码不是一一对应的。

通过编译器输出汇编语言的源代码

大部分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行就可以完成的事,汇编语言需要多行,效率很低。不过,汇编语言的经验还是很重要的。因为借助汇编语言,我们可以更好地了解计算机的机制。特别是对专业程序员来说,至少要有一次使用汇编语言的经验。

posted @ 2021-04-06 17:11  睿阳  阅读(238)  评论(0编辑  收藏  举报