程序是怎样跑起来的-第10章 通过汇编语言了解程序的实际构成

第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语句,大家没有必要这么紧张。

时间: 2024-12-10 10:10:17

程序是怎样跑起来的-第10章 通过汇编语言了解程序的实际构成的相关文章

《程序是怎样跑起来的》第一章有感

在没有读<程序是怎样跑起来的>,这本书之前,我对于第一章所讲解CPU在脑子中只是知道它相当于是计算机的大脑,内部由数百万至数亿个晶体管构成.这本书在开始是就先对CPU的内部结构进行了解析,知道了CPU的内部是由寄存器,控制器,运算器和时钟四部分构成,各部分之间由电流信号相互联通.后来又向我们解释了内存,接着有告诉我们CPU是寄存器的集合体,而这一过程中最主要的就是了解寄存器是程序把其作为对象来描述的. 通过这一部分知识的学习,使我对CPU的内部结构又了初步的了解,虽然还是模糊的概念,但是积少成

《程序是怎样跑起来的》第一章读后感

读了<程序是怎样跑起来的>这本书的第一章之后,让我对CPU的理解更加深入.刚开始我只认为它是相当于计算机的大脑,原来它对于程序员来说不止如此,它还是CPU,寄存器,内存,内存地址,程序计数器,累计寄存器,标志寄存器和基址寄存器.它的内部是由寄存器,控制器,运算器和时钟四部分构成. 平常上课的时候我只是知道老师让我们往电脑上敲代码,敲完后运行成功,自己再加深理解,懂得如何运行成功的就行了.但是通过对这一章的学习,我懂得了程序是怎么运行,它的运行原理.CPU的处理很简单,但是程序还是要靠我们的编程

程序是怎样跑起来的第7章有感

读<程序是怎样跑起来的>第七章有感 本章主要讲的是程序是在环境下运行的内容,首先操作系统和硬件决定了程序的运行环境,机器语言的编码被称为本地代码,程序员用C语言等编写的程序,在编写的阶段仅仅是文本文件,文本文件在任何情况下都能显示和编辑,称之为源代码,通过对源代码进行编译,就可以得到本地代码.CPU负责解析并运行从源代码编译而来的本地代码. Windows的前身操作系统是20世纪80年代的MS-DOS操作系统的广泛使用的时代,在MS-DOS的时代,如果想使用当时大热的文字处理软件----Jus

读《程序是怎样跑起来的》第二章有感

在学c#的时候书上提到过左移右移,不过并没有作为重点来讲,只是大概地说了一下是什么含义.刚看第二章作者提出的六个问题就有四个难住了我.比如一串二进制数左移两位会变成原数的几倍.还有补码形式表示的八进制如何用二进制表示等等,这些问题一点都不会.不过看完答案和作者给出的解析后大概理解了. 第一小节讲了计算机为什么用二进制表示信息,然后第二节讲了什么是二进制数,以及二进制转化为十进制的方法,接着后面讲了如何使用移位运算代替乘除运算,现在我理解老师上课说计算机中也可以使用乘除运算是怎么回事了.最后是我最

《程序是怎样跑起来的》第二章读后感

        本章开关说明了"要想对程序的运行机制形成一个大致印象,就要了解信息在计算机内部是以怎样的形式来表现的."并讲解了计算机为什么用二进制数表示.什么是二进制数.二进制数是如何运算的.而后讲解的移位运算和乘除运算的关系,使我明白了通过左移右移可以代替了乘法运算和除法运算,接下来的逻辑运算是重中之重的,可能大家听到这个词会觉得有点难,但看懂了的话是比较简单的,逻辑运算其实就是对0和1进行处理的运算,而掌握逻辑运算的窍门就是不把二进制数当成数值,而是看成一个开关,刚开始也许会不适

《程序是怎样跑起来的》第一章

第一章的主要内容就是对CPU进行了详细的解释.CPU相当于计算机的大脑,它有数百万至数一个晶体管构成.CPU的内部主要有控制器,运算器,寄存器和时钟构成.如寄存器可用来暂存指令.数据等处理对象可以将其看作是内存的一种,控制器负责把内存上的指令.数据等读人寄存器,并根据指令的执行结果来控制整个计算机.运算器负责运算从内存读人寄存器的数据 . 在第一章开始介绍了我们学习的高级语言计算机并不能直接识别.将高级语言进行编译成机器语言.然后计算机才能进行识别.虽然老师以前也讲过相关的知识.但是读过这篇文章

读《程序是怎样跑起来的》第一章有感

程序是只是计算机每一步动作的指令,计算机内部的编译和代码是人工事先做好的,我们输入代码,计算机内部进行编译成机器语言,执行代码.而cpu是计算机的核心部位,cpu能够直接识别和执行的只有机器代码,像我们输入的java代码和c#代码都是进行转化为机器语言之后才会运行的.而cpu是由许多晶体管组成的,内存是计算机的主存储器,通过控制芯片与cpu相连.内存都会有地址,通过引用地址对数据进行处理. 读完第一章后,我对程序的运行有了一个大概的印象,程序怎么运行,在脑子里有了一个模型,并且对程序的组成和计算

《程序是怎么跑起来的》第二章有感

在计算机中,数据是由二进制来表示的.我们生活在常用的是十进制,当我们把十进制数输入到计算机中当计算机执行时计算机就会把我们输入的十进制数转换成相应的二进制数进行计算,计算完成在把二进制数转换成十进制数输出在屏幕上,在程序中,不仅是十进制数是转换成二进制文字和图像等信息在计算机内部都是以二进制数值的形式来表现的,通过电流符号,我们可以给CPU发送指令,或者传递数字信息,读完这一章感觉重点和难点在移位运算和乘除运算的关系以及补位,掌握移位运算和乘除运算的关系以及补位这一章就算学会了. 原文地址:ht

读《程序是怎么跑起来的》第一章有感

从功能方面来看,CPU的内部由寄存器.控制器.运算器和时钟四个部分构成,各部分之间由电流信号相互连通.程序是把寄存器作为对象来描述的.寄存器有许多不同的种类,与其种类相对应的是其不同的功能. 程序的流程分为循序执行,条件分支和循环三种. cpu的执行比较是由cpu的运算装置自动实行减法运算后得出的结果. 使用call和return可以很好地解决函数调用的问题. 原文地址:https://www.cnblogs.com/zawpl/p/10327141.html