通过汇编程序理解汇编和链接过程

一个标准C程序到最终的可执行文件包含如下几个过程:
通过汇编程序理解汇编和链接过程_第1张图片
但今天我们要从汇编程序开始讨论,所以不讨论预编译和编译过程,直接从汇编代码开始,讨论汇编和链接过程。

汇编代码简析

通过编写汇编程序,然后分析它的汇编和链接过程,对理解汇编程序中的各种汇编器指令和各种标签很有帮助。

首先介绍一下汇编器指令和标签这两个概念,观察下面一段求最大值的汇编程序代码maxmum.s:

#目的:寻找一组数中的最大值

#寄存器有以下用途:
#		%edi - 保存正在检测的数据的索引
#		%ebx - 当前已经找到的最大数据项
#		%eax - 当前数据项

#使用以下内存位置:
#	data_items - 包含数据项
#		     0表示数据结束

.section .data

data_items:
.long		 3, 67, 34, 222, 45, 75, 54, 34, 44, 33, 22, 11, 66, 0

.section .text
.global _start		#不加.globl,表示只在本文件中有效。加了.globl修饰,表示_start标签在其他文件也有效。在多个.o目标文件链接时,其代表的地址可以被引用。

_start:
		movl 		$0, %edi
		movl 		data_items(,%edi,4), %eax
		movl 		%eax, %ebx

start_loop:
		cmpl		 $0, %eax	
		je 			loop_exit	
		incl 		%edi
		movl 		data_items(,%edi,4), %eax
		cmpl 		%ebx, %eax	
		jle 			start_loop
		movl 		%eax, %ebx
		jmp		start_loop	

loop_exit:
		movl 	$1, %eax
		int 		$0x80

在这段代码中,.section, .long就是汇编器指令,它们是伪汇编指令,是帮助汇编器和链接器做处理的,在底层没有与之对应的机器码。而movl指令是真正的汇编指令,在底层有对应的机器码,可以真正被处理器执行。

_start, start_loop, loop_exit这三个符号后面都有一个冒号“:”,表示它们是标签。汇编代码中的标签,代表的含义就是内存地址。稍后通过反汇编它们链接而成的ELF文件,可以观察到这些标签起到的作用。

接下来,将这段汇编代码使用as汇编器汇编,将所有的汇编指令翻译成机器码构成的.o目标文件:

as	maxmum.s 	-o	maxmum.o

接着,使用链接器ld将.o目标文件进行链接,由于这里不涉及库函数调用,所以需要链接的目标文件仅有一个:

ld	maxmum.o	-o	maxmum

经过链接以后,形成了最终可以被linux内核识别的可执行文件格式ELF,其实就是将刚才汇编代码中的所有标签换成它所代表的内存地址,稍后会在反汇编时看到这一点。

使用objdump反汇编Linux的可执行文件

使用objdump工具进行反汇编,查看所有可执行节(.section)的信息:

objdump		-d	maxmum

可执行文件反汇编后显示如下:
通过汇编程序理解汇编和链接过程_第2张图片
注意两列红色箭头指向的信息,左面是一个内存地址(虚拟地址),右面是该地址对应的标签。在上面提到过,汇编程序中一个标签就代表一个地址,所以在这里看到的就是汇编程序中的标签对应的地址。

在汇编程序的.text节中,我们定义了3个标签,分别是_start,start_loop,和loop_exit,这正是我们在反汇编可执行文件后看到的<_start>,,所以,以后再看到反汇编ELF文件中的标签,就知道它们是怎么来的了。

标签替换

如果再观察反汇编后的汇编语句,对比我们写的汇编程序中的汇编语句,你有没有发现了什么?
观察下面的两张图,上面为汇编程序中的汇编代码,下面是对应的可执行文件反汇编后的结果:

  1. 汇编程序中的标签
    通过汇编程序理解汇编和链接过程_第3张图片
  2. 反汇编中的标签
    通过汇编程序理解汇编和链接过程_第4张图片
    值得注意的是,对于标识数据段的标签,如汇编代码中的data_items,会直接替换为地址;对于标识代码段的标签,也会被替换为地址,但是在地址后面会显示对应的标签,这样在分析汇编程序时,可以更方便的观察控制流的跳转。

所以根据上面的分析,我们可以知道两点知识:

  1. 汇编代码中的标签代表的含义是地址,并且在汇编和链接的时候会由汇编器和链接器将标签替换为地址。
  2. 一般地址后没有跟其对应的标签的,这个地址是数据段(.section .data)的地址;如果地址后跟了一个标签,如jmp 4000bf ,这个地址就是代码段(.section .text)中的一个标签地址。

现在我们来实际观察一个标准的C程序编译成的ELF文件反汇编后的结果:

在这里插入图片描述
先来看下第一张图,红线标出了三个标签,现在,我们并不关心这三个标签在实际的汇编代码中的作用,只需要知道这三个标签代表的是代码段中的地址,是在控制流跳转的时候的发挥作用。现在在看反汇编的结果时,是不是感觉清楚了一些呢。

第一张图和第二张图各用红色框圈出了两个地址,但是第一个地址0x200a91后没有跟标签名,所以这是一个数据段的内存地址;而第二个地址5b0后跟了一个标签,说明这个地址是代码段中的一个地址。

有趣的链接

链接的原理其实很简单,就是把不同的目标文件中的标签全部保存在一个新的文件中,这个新文件就是可执行文件,在linux中可执行文件的格式为ELF。

现在,我们继续看一段代码,还是刚才的计算最大值的汇编程序:
通过汇编程序理解汇编和链接过程_第5张图片
我们用保存返回值的ebx寄存器来保存最大值结果。汇编和链接以后,使用./maxmum运行。然后通过echo $?查看刚才程序的退出状态码。结果如下:
在这里插入图片描述
最大值为222,程序运行正确。

在上面的汇编程序中,我们把代码段和数据段放在了一个文件中,并且在汇编的时候,不需要依赖其他的文件,所以汇编以后只有一个.o目标文件,在链接的时候,只需要链接这一个.o目标文件,这样并不能直接体会到链接的作用,所以如何再增加一个.o目标文件呢?

最简单的办法就是把上面的汇编代码拆成两个部分,将代码段和数据段分离,即变成下面这样:

  • 代码段
    通过汇编程序理解汇编和链接过程_第6张图片
  • 数据段
    通过汇编程序理解汇编和链接过程_第7张图片
    代码段的汇编源程序为maxmum_text.s,数据段的汇编源程序为maxmum_data.s,如下:
    在这里插入图片描述

分别对代码段汇编代码和数据段汇编代码进行汇编:

as	maxmum_text.s	-o	maxmum_text.o
as	maxmum_data.s	-o	maxmum_data.o

各生成一个.o目标文件,如下:
在这里插入图片描述
现在是体现链接作用的时候了,我们现在有两个目标文件了,一个是代码段的,一个是数据段的,现在我们将它们链接形成一个可执行文件,看它会不会实现上面求最大值的功能:

ld	maxmum_text.o	maxmum_data.o	-o	maxmum_test

链接之后生成了一个可执行文件:
在这里插入图片描述
运行然后查看结果:
在这里插入图片描述
可以看到,虽然我们将代码段和数据段分开了,但最后经过链接器链接后,形成的可执行文件依然实现了求最大值的功能。

所以这里链接器的作用已经很明显了,就是将不同文件中的标签组合在一个文件中,比如在代码段中,有_start标签,标识代码的起始位置,再数据段中,有data_items标签,标识数据段的起始位置,它们本来是各自存在于自己的目标文件中,但是经过链接器链接以后,这个时候再去反汇编形成的可执行文件,你会发现这些标签被组合在一起了,并且数据段标签data_items已经被直接替换成了地址0x6000dd:
通过汇编程序理解汇编和链接过程_第8张图片
但是这里需要有一点需要注意,在链接的过程中,因为要将不同文件中的标签组合在一起,所以在汇编的时候标签要通过.globl进行修饰,表示这个标签稍后会在链接过程中用到,让汇编器不要丢弃这个符号。

这里我们对比一下代码段与数据段合在一起的汇编程序中数据段的标签data_items,和单独存放数据段的汇编程序中的数据段标签data_items:

  1. 代码段与数据段合在一起的汇编程序中数据段的标签data_items
    通过汇编程序理解汇编和链接过程_第9张图片
  2. 单独存放数据段的汇编程序中的数据段标签data_items
    通过汇编程序理解汇编和链接过程_第10张图片
    加了.globl修饰之后,汇编过程结束后,标签data_items代表的地址不会被汇编器丢弃,所以在链接的时候,当另一个文件中引用这个标签的时候,这个地址可以找到,所以引用这个地址的时候就不会出错。

相反,如果我们在将数据段分出来后,不用.globl修饰data_items标签,会发生什么呢?接下来就通过实验来观察,去掉单独存放数据段的汇编程序中的数据段标签data_items的.globl修饰:
通过汇编程序理解汇编和链接过程_第11张图片
现在我们重新进行汇编和链接,汇编只需要对数据段在汇编即可,因为代码段不涉及改动:
在这里插入图片描述
不出意外,在链接的时候报错了,因为在代码段生成的.o文件中,需要引用数据段生成的.o文件的data_items标签代表的地址,但是我们并没有对这个标签使用.globl进行修饰。

这个错误相信C程序员一定不会陌生,对’data_items’未定义的引用,就是当链接器想要替换这个标签的时候,不知道这个标签对应的地址。再直接一点,就是找不到存放这个数据的内存单元的编号,即内存地址,即使链接器不进行拦截,日后CPU注定在这里访问会出错,所以这种错误应当在链接的时候就要被捕获到。

关于程序的大小

当我们在查看一个C程序的大小时,究竟在查看什么?我们先使用size命令查看一个可执行文件的大小,以上面的最大值的可执行文件为例:
在这里插入图片描述
其中,text列显示的是代码段的大小,为45字节;data列显示的是数据段的大小,为56字节,因为在求最大值的汇编程序中,并没有未初始化的内存单元,所以bss列为空。

接下来,我们就来看一下代码段的大小和数据段的大小是怎么得来的,首先再回到求最大值的汇编源程序,看一下数据段的定义:
通过汇编程序理解汇编和链接过程_第12张图片
在data_items里时我们定义的数据区,.long表示是编译器指令,定义一个4字节的数据,这里面定义了14个数据,分别是3,67,34,222,45,75,54,34,44,33,22,11,66,0,所以数据区大小=14*4=56Byte。

现在我们把.long改为.byte,每个数据都只用一个字节来存放:
通过汇编程序理解汇编和链接过程_第13张图片
重新汇编和链接,再使用size命令查看大小:
在这里插入图片描述
现在数据区的大小变成了14字节。还可以使用.word来定义数据,.word表示双字(节),这时数据区大小会变为28字节。
在这里插入图片描述

下面,我们来看代码段的大小,先把求最大值程序的可执行文件反汇编:
通过汇编程序理解汇编和链接过程_第14张图片
中间的一列是由十六进制数字表示的字节码,一个字节码表示一个字节。后面的黄色数字表示该行有几个机器码,即几个字节。现在,把这些字节数相加:5x2+7x2+2x8+1x2+3=45,再对比反汇编中代码段的大小:

在这里插入图片描述
所以,代码段的大小就意味着程序的代码量。

有趣的世界,隐匿在01背后

这就要从机器码说起,机器码是由01组成的序列,长这样:0101 10001,这里面0代表低电平,1代表高电平,是数字信号,但是处理器是电驱动的(想想你的计算机为什么经常用一会就没电了),所以01数字信号需要经过数模转换器转换成模拟信号,即电信号,然后驱动着处理器内部的逻辑电路工作,逻辑电路是由与门,或门和非门组成的复杂的逻辑门电路,本质就是通过电流的通断来控制晶体管开关的闭合,进而控制不同的与门,或门和非门工作。处理器的内部有ALU(算术逻辑单元,实际执行指令的地方),寄存器,这些功能单元其实就是由与门,或门和非门组成的逻辑门电路。通过电流控制这三种门组成的逻辑电路工作,就可以实现寄存器的存数,和ALU进行加法进位等过程,这也是计算机运行时,处理器内部真实发生的事情。

现在,通过上面的描述,你应该了解了处理器内部是怎么工作的了,关于这个问题,即计算机是怎么运行起来的,可以看我之前写过一篇文章:计算机是怎么运行的?为什么它可以自动化的工作?这和时钟信号又有什么关系?

理解了计算机是怎么工作的之后,相信你也深刻体会到了机器码的意义。

以上就是关于在汇编程序中理解理解汇编和链接过程一点小小总结。

你可能感兴趣的:(汇编)