第十章——通过汇编语言了解程序的实际构成(二)

原创
2020/10/15 21:26
阅读数 65

章节结构

  • 始终确保全局变量用的内存空间  

  • 临时确保局部变量用的内存空间

  • 循环处理的实现方法

  • 条件分支的实现方法  

  • 了解程序运行方式的必要性

始终确保全局变量用的内存空间  

C语言中,在函数外部定义的变量称为全局变量。在函数内部定义的变量称为局部变量

// 定义被初始化的全局变量
int a1 = 1;
int a2 = 2;

// 定义没有初始化的全局变量
int b1,b2;

//定义函数
void MyFunc()
{
	// 定义局部变量
	int c1,c2,c3,c4;
	
	// 给局部变量赋值
	c1 =1;
	c2 = 2;
	c3 = 3;
	c4 = 4;
	
	// 把局部变量的值赋给全局变量
	a1 =c1;
	a2 = c2;
	b1 = c3;
	b2 = c4;
}

汇编语言

_DATA segment dword public use32 'DATA'
_a1 label  dword                                     (4)
      dd         1                                   (5)
_a2   dd         1

_DATA ends

_BSS segment dword public use32 'BSS'
_b1   label    dword
         db       4   dup(?)                         (6)
		 
_b2   label    dword
         db       4   dup(?)                         (6)
_BSS ends

_TEXT segment dword public use32 'CODE'
_MyFunc          proc         near
      push                 ebp
	  move                 ebp,esp
	  
_MyFunc         endp
_TEXT           ends
操作码 操作数 功能
add A,B 把A的值和B的值相加,并把结果存入A
call A 调用函数A
cmp A,B 对A和B的值进行比较,比较结果会自动存入标志寄存器中
inc A A的值加1
jge 标签名 和cmp命令组合使用。跳转到标签行
ji 标签名 和cmp命令组合使用。跳转到标签行
jle 标签名 和cmp命令组合使用。跳转到标签行
jmp 标签名 将控制无条件跳转到指定标签行
mov A,B 把B的值赋给A
pop A 从栈中读取出数值并存入A中
push A 把A的值存入栈中
ret 将处理返回到调用源
xor A,B A和B的位进行一伙比较,并将结果存入A中

编译后的程序会被归类到名为段定义的组。在Borland C++使用 规范中:

  • 初始化的全局变量,会被汇总到名为_DATA的段定义中,

  • 没有初始化的全局变量,会被汇总到名为_BSS的段定义中

  • 指令会被汇总到名为_TEXT的段定义中。

  • _DATA segment和_DATA ends、_BSS segment和_BSS ends、_TEXT segment和_TEXT ends,这些都是表示各 段定义范围的伪指令。

  • 编译后的函数名和变量名前附加一个下划线

  • 规定int类型的长度是4字节。

  • 程序运行时没有初始化的全局变量的领域(_BSS段定义)都会被设定为0进行初始化。

标签表示的是相对于段定义起始位置的位置。

_DATA段定义的内容。(4)中的_a1 label dword 定义了_a1标签。
由于_a1在_DARA段定义的开头位置,所以相对位置是0。_a1相当于全局变量a1。

(5)中的dd 1指的是,申请分配了4字节的内存空间,存储着1这个初始值。
dd(define double word)表示的是由两个长度为2的字节领域(word),也就是4字节。这里也定义了相当于全局变量a1、a2的标签_a1、_a2。它们各自的初始值也都被存储在4字节的领域中。

_BSS段定义的内容 定义了相当于全局变量b1、b2的标签_b1、_b2. (6)的db 4 dup(?)表示的是申请分配了4字节的领域,但值尚未确定(这里用?来表示)。
db(define byte)表示有1个长度是1字节的内存空间。
因而db 4 dup(?)表示的就是4个长度是1字节的内存空间
db 4 表示的是双字节(= 4字节)的内存空间中存储的值是4.

从上面_DATA和_BSS的段定义中,全局变量的内存空间都得到了确保。因而,从程序的开始到结束,所有部分都可以参阅全局变量。

在Borland C++中,程序运行时没有初始化的全局变量的领域(_BSS段定义)都会被设定为0进行初始化。
所以根据是否进行了初始化把全局变量的段定义划分为了两部分。
通过汇总,初始化很容易实现,只要把内存的特定范围全部设定为0就可以了。

临时确保局部变量用的内存空间

局部变量只能在定义该变量的函数内进行参阅

因为,局部变量是临时保存在寄存器和栈中的。

  • 函数内部利用的栈,在函数处理完毕后会恢复到出示状态,因此局部变量的值也就被销毁了,
  • 而寄存器也可能被用于其他目的。

寄存器空闲时使用寄存器,寄存器空间不足时就是用栈。

Borland C++编译器对于局部变量会尽可能的使用寄存器,这是最优化的运行结果。

(8)表示的是往寄存器分配局部变量的部分
仅仅对局部变量进行定义是不够的,只有在给局部变量赋值时,才会被分配到寄存器的内存区域。 当寄存器不多而局部变量很多时,寄存器只是被单纯地用于存储变量的值,与其本身的角色没有任何关系

x86xilie CPU 拥有的寄存器中,程序可以操作的有十几个。其中空闲的,最多也只有几个。因而,局部变量数目很多的时候,可分配的寄存器就不够了。这种情况下,局部变量就会申请分配栈的内存空间。虽然栈的内存空间也是作为一种存储数据的段定义来处理的,但在程序各部分都可以共享并临时使用这一点上,它和_DATA、_BSS段定义在性质上还是有些差异的。 如:在函数入口处位变量申请分配栈的内存空间的话,就必须在函数出口处进行释放。否则,经过多次调用函数后,栈的内存空间就会被用光了。

mov eax,1
mov edx,2
mov ecx,3
mov ebx,4

循环处理的实现方法

通过利用for循环以及if条件分支来改变程序流程的机制称为流程控制
循环计数器:用来计算循环次数的变量。

对比C语言和汇编语言的源代码

// 定义MySub()函数
void MySub()
{
	// 不做任何处理
}

// 定义MyFunc函数
void MyFunc()
{
	int i;
	for (i = 0;i < 10;i++)
	{
		//重复调用MySub()函数10次
	}
}
// for语句转换成汇编语言
           xor           ebx,ebx                 ;将eax寄存器请0
@4	   call          _MySub                 ;//调用MySub函数
           inc           ebx                    ;//ebx寄存器的值加1
	   cmp           ebx,10                   ;//将ebx寄存器的值和10进行比较
	   jl            short     @4          ;//如果小于10   就跳转到@4

MyFunc 函数中用到的局部变量只有i,变量i申请了ebx寄存器的内UC你空间。
for语句的括号中的i = 0;被转换成了 xor ebx,ebx这一处理。
XOR为异或运算,即同1为0,同0为0,其他为1(相同数值为0,不同数值为1)
因此不管当前ebx寄存器的值是什么,记过肯定为0。move ebx,0也可以得到相同的结果。但是xor指令处理的速度更快。(这里,体现了编译器的最优化功能)

ebx寄存器的值初始化后,会通过call指令调用MySub函数(_MySub)。从MySub函数返回后,则会通过inc 指令对ebx寄存器的值做加1处理。相当于for语句中i++

下一行的cmp指令是用来对第一个操作数和第二个操作数的数值比较的指令。
cmp ebx,10就相当于C语言的i<10这一处理,意思是把ebx寄存器的数值同10进行比较
汇编语言比较指令的结果,会存储在CPU的标志寄存器中
但是,标志寄存器的值,程序是无法直接参考的

程序是怎么判断比较结果的?

  • 汇编语言比较指令的结果,会存储在CPU的标志寄存器中
  • 但是,标志寄存器的值,程序是无法直接参考的
  • 汇编语言中有多个跳转指令,这些跳转指令会根据标红字寄存器的值来判定是否需要跳转。

如最后一行的jl,是jump on less than(小于的话就跳转)的意思。
即jl short @4表示,前面运行的比较指令的结果若“小”的话就跳转到@4这个标签

条件分支的实现方法  

条件分支的实现方法同循环处理的实现方法类似,使用的也是cmp指令和跳转指令。

  • 进行条件分支的C语言源代码
// 定义MySub1函数
void MySub1()
{
	// 不做任何处理
}

// 定义MySub2函数
void MySub2()
{
	// 不做任何处理
}

// 定义MySub3函数
void MySub3()
{
	// 不做任何处理
}

// 定义MyFunc函数
{
	int a = 123;
	// 根据条件调用不同的函数
	if(a > 100)
	{
		MySub1();
	}
	else if(a < 50)
	{
		MySub2();
	}
	else
	{
		MySub3();
	}
}

  • MyFunc函数转换成汇编语言后的结果
_MyFunc          proc               near
      push          ebp;
	  mov           ebp,esp;
	  mov          eax,123           ;把123存入eax寄存器中
	  
	  cmp          eax,100           ;把eax寄存器的值同100进行比较
	  jle             short  @8        ;比100小时,跳转到@8标签
	  call           _MySub1         ;调用MySub1函数
	  
@8       cmp           eax,50              ;把eax寄存器的值同50进行比较
         jge           short @10            ;大于50时,跳转到@10标签
        call          _MySub2             ;调用MySub2函数
	jmp       short @11            ;跳转到@11标签  
		
@10	   call           _Mysub3             ;调用MySub3函数
	   
@11	   pop       ebp
	   ret 
_MyFunc     endp


// eax寄存器表示的是变量a

cmp:用来比较的指令。
比较结果被保存在标志寄存器。

三种跳转指令:

  • jle(jump on less or equal) 比较结果小时跳转
  • jge(jump on greater or equal):比较结果大时跳转
  • jmp:不管比较结果怎样都无条件跳转
    C语嫣中goto相当于jmp指令

了解程序运行方式的必要性

展开阅读全文
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部