x86汇编程序基础

本文深入介绍汇编知识,涵盖简单汇编程序编写、编译与链接,x86寄存器特点,第二个汇编程序求数组最大值,多种寻址方式,以及ELF文件类型(可重定位、可执行)。详细分析了汇编、链接、执行过程,对比目标文件和可执行文件的差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  • 要彻底搞清楚C语言的原理,就必须深入到指令一层去了解,写一行代码编译器会生成什么样的指令,要做到心中有数。

最简单的汇编程序

#PURPOSE: Simple program that exits and returns a
#	  status code back to the Linux kernel
#
#INPUT:   none
#
#OUTPUT:  returns a status code. This can be viewed
#	  by typing
#
#	  echo $?
#
#	  after running the program
#
#VARIABLES:
#	  %eax holds the system call number
#	  %ebx holds the return status
#
	.section .data
	.section .text
	.globl _start
	_start:
		movl $1, %eax	#	this is the linux kernel comand
						#	number ( system call )		for	exiting
						#	a program
		movl $4, %ebx	#	this	is	the	status	number	
						#	we will	return	to	the	operating	system.
						#	Change this around and it will return different things to
						#	echo	$?
		int $0x80		#	this	wakes	up	the	kernel	to	run
						#	the	exit	command	

把这个文件保存为hello.s(汇编通常以.s作为文件的后缀名),用汇编器(Assembler)as 把汇编程序中的助记符翻译成机器指令,生成目标文件 hello.o:

  • $ as hello.s -o hello.o

之后使用连接器(linker)将目标文件链接成可执行文件 hello:

  • $ ld hello.o -o hello

这里我们发现和编译C不同的是,汇编语言在翻译成机器语言之后还有要链接,那么链接的作用是什么呢?首先用于修改目标文件中的信息,对地址作重定位,二是把可执行文件合并成可执行文件。这两点都会在后边详细解释。上述例子虽然只有一个目标文件,但也需要经过链接才能成为可执行文件。

上边的程序只做了一件事,就是退出,退出状态是4,我们可以用以下指令检验:

  • $ ./hello
  • $ echo $?
  • 4

实际上这个程序相当于在C语言中的main函数中返回4,后续会进行详细解释。


下面进行详细解释。
首先对于 # ,相当于C语言中的 // 注释。

.section	.data

在汇编程序中由.开头的名称并不是助记符,不会被编译成机器指令,而是用于给汇编器一些指示,称为汇编指示伪操作。其中section是用于便于汇编器将程序分段,之后被操作系统加载到不同的页,给不同的段设置不同的权限。.data是用于声明变量的,相当于C语言中的全局变量。由于本段没有定义数据,所以这一部分为空。

.section .text

.text是用于存放代码的段,是只读、可执行的,后边的指令都属于.text段。

.globl _start

这里声明了一个符号,_start是一个符号,符号在汇编语言中代表一个地址,可以用在指令中,在汇编之后所有的符号都会被替换为相应的地址,就像在C语言中我们用变量名来访问变量,其实就是在访问变量所在的地址,调用函数,就是跳转到函数的第一条指令所在的地址,所以变量名和函数名都是符号,代表相应的地址。
.globl指示是用于告诉汇编器,_start要被链接器用到,所以要在目标文件的表中标记它是一个全局符号。_start就相当于C语言中的main函数,是程序的入口,需要使用.globl声明,链接器在链接过程中要去寻找_start作为程序入口。如果一个符号没有用.globl声明那么就不会被链接器用到。

_start:

这里定义_start,一个符号代表一个地址,汇编器在汇编时会计算每一个数据对象和指令的地址,当看到这样一个符号定义时,就会把它后面一条指令的地址赋给它。而_start又是比较特殊的一个符号,是程序的入口,所以下一条指令的地址也就是整个程序中第一个被执行的指令的地址。

movl	$1,	%eax

这是一条数据传输的语句,这条指令是生成一个立即数1并将其传入eax寄存器。mov的后缀 l 表示long,说明是32为传输指令。立即数指的是CPU内部产生。在汇编程序中寄存器要加%,立即数要加$,以便和其他符号区分开。mov指令还有几种形式,不过传输方向是不变的,第一个操作数都是源操作数,第二个都是目标操作数。

movl 	$4,%ebx

这条指令和上一条类似,将立即数4传入ebx。

int $0x80

前两条指令是为这条指令做准备,执行这条指令时执行下面的动作:
1.int称为软中断指令,是指故意产生一个异常,这种异常称为系统调用,CPU切换用户模式到特权模式,进入内核执行相应的处理异常的程序。
2.int指令中的$0x80是一个参数,为了系统的安全性,用户不能随便的调用内核函数,只能传递几个存储在寄存器里的参数,就想调用函数一样,之后继续执行下一条指令。
3.eax和ebx的值是系统调用的两个参数,eax是系统调用号,Linux中系统调用都是由int 0x80引发的,1所对应的是_exit。ebx的值是传给_exit的参数,表示退出状态。大多数系统调用都会在调用完之后返回用户空间,继续下面的指令,而_exit比较特殊,直接结束程序,而不返回用户空间。


x86的寄存器

x86通用寄存器有eaxebxecxedxediesi。对于通用寄存器大多数指令是可以任意使用的,比如movl指令可以移动到eax也可以移动到ebx。但对于一些特殊的指令只能使用其中几个通用寄存器,例如除法指令idivl要求被除数在eax中,edx必须是0,而除数可以在任意寄存器中,因此对于特殊指令通用寄存器并不通用。
除通用寄存器外还有特殊寄存器,ebpespeipeflagseip是程序计数器,eflag用于记录运算过程中的标志位,如进位标志,负数标志,以及零标志。ebpesp用于维护函数调用栈帧。


第二个汇编程序

求一组数的最大值:

#PURPOSE: This program finds the maximum number of a
#	  set of data items.
#
#VARIABLES: The registers have the following uses:
#
# %edi - Holds the index of the data item being examined
# %ebx - Largest data item found
# %eax - Current data item
#
# The following memory locations are used:
#
# data_items - contains the item data. A 0 is used
# to terminate the data
#
.section	.data
data_items:		#These are the data items
.long  3,67,34,222,45,75,54,34,44,33,22,11,66,0

.section .text
.globl	_start
_start:
	movl $0,%edi	#	mov index into inde register
	movl data_item(,%edi,4), %eax	#	load the first byte of data
	movl %eax, %ebx	#	since this is the first item, %eax is the biggest,
					#	 %ebx store the biggest
	
	start_loop:	#	start loop
		cmpl $0, %eax	#	check to see if we`ve hit the end
		je loop_exit
		incl %edi	#	load next value
		movl data_items(,%edi,4), %eax
		cmpl %ebx, %eax	  #	compare value
		jle start_loop	# jump to loop begging if the new one isn`t the bigger
		movl %eax, %ebx 	# move the value as the biggest
		jmp start_loop	# jump to loop begging
	
	loop_exit:
		#	%ebx is the status code for the _exit system call
		#	and it already has the maximum number
		movl $1, %eax	# $1 is the _exit() syscall
		int 0x80

这个程序寻找一组数的最大值并将它作为程序退出状态。这组数据在.data段给出

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

.long指示声明一组数,每个占32位(IPL32),相当于数组。这个数组开头定义了一个符号data_items,因此该符号所代表的地址就是这个数组元素的首地址,类似于数组名。没有使用globl声明是因为只在汇编程序内部使用,而链接器并不需要是用这个名字。
除了.long之外,常用的数据声明有:

  • .byte用于声明一个八位的数组。
  • ascii,用于声明一个有asc码组成的数组,例如.ascii "hello world\0",这里就声明了一个12个元素的数组,相当于一个字符串,同时要注意的是必须要有’\0’才是字符串,汇编语言不会像C语言一样默认加上’\0’。

data_items数组最后一个数是零,是用于终止循环的,在遇到0时就结束循环。

寄存器作用:

  • edi 用于保存数组中的当前位置,即偏移量,每次比较完之后加一,一遍访问数组的下一个数。
  • eax 用于临时存储当前访问数组的数,用于和ebx中的值比较。
  • ebx 用于存储当前的最大值,如果与eax比较,比eax的值小,则用eax的值更新ebx

下面对text段进行分析

	_start:
		movl $0, %edi

初始化edi,将偏移量设置为0,用于指向数组首元素。

movl data(,%edi,4), %eax

将当前访问的数组的元素存入eax中。data_items是数组的首地址,edi是偏移量,4是偏移的单位,所以访问的地址就应该是 data_items + edi * 4。

movl %eax, %ebx

将第一个元素作为最大值。

下面进入循环,循环开始定义一个符号start_loop,循环的结尾之后定义一个符号loop_exit

start_loop:
	cmpl $0, %eax
	je loop_exit

判断eax是不是零,如果是说明到达数组末尾了,就要跳出数组。cmpl指令将两个操作数相减,但计算结果不保存,只是改变eflag中的标志位。如果两个数相等那么eflag中的ZF位置为1。je是一个跳转指令,他检查eflag中的ZF位,如果为1则跳转,如果为0则不跳转,继续执行下一条指令。可见比较指令和跳转指令是一同使用的,前者改变标志位,后者根据标志位判断是否跳转。je可以理解成“jump if equal”。

incl %edi
movl data_items(,%edi,4), %eax

将edi的值加一,并将当前访问数组的元素存入eax。

cmpl %ebx, %eax
jle start_loop

把当前元素与最大值比较,如果前者小于等于后者,则最大值不变,并跳到循环开始,否则执行下一条指令。这里jle可以理解为“jump if less or equal”。

movl %eax, %ebx
jmp loop_start

将最大值更新,并返回循环开始处。jmp是一个无条件跳转指令,不需要判断任何条件。

循环结束之后,调用_exit系统函数退出程序。


寻址方式

内存寻址在指令表中可表示成下面的格式:

ADDRESS_OR_OFFSET(%BASE_OR_OFFSET,%INDEX,MULTIPLER)

他所表示的地址:
FINAL ADDRESS = ADDRESS_OR_OFFSET + BASE_OR_OFFSET + INDEX*MULTIPLIER
其中ADDRESS_OR_OFFSET和MULTIPLIER必须为常数,BASE_OR_OFFSET和INDEX必须为寄存器。有些寻址方式会省略其中一部分,并当作是0。

  • 直接寻址,只是用ADDRESS_OR_OFFSET进行寻址,例如 movl ADDRESS, %eax 中直接将ADDRESS处的值传入。
  • 变址寻址,例如前边所用的 movl data_items(,%edi,4) 就是这种方式,便于数组的访问。
  • 间接寻址,只是用BASE_OR_OFFSET,例如 movl (%eax), %ebx,把eax寄存器中的值作为地址进行访问,与 movl %eax, %ebx不同,%eax不加括号时把eax中的值看成值,加括号是看成地址。
  • 基址寻址,只是用ADDRESS_OR_OFFSET和BASE_OR_OFFSET寻址,例如 4(%eax),例如一个结构体的基地址保存在eax中,其中一个成员在结构体中的偏移量是4字节,要把这个成员读上来就可以用这条指令。
  • 立即数寻址,就是指令中有一个立即数,例如 movl $666, %eax,其中$666就是立即数,虽然和寻址没有关系,但是也算一种寻址方式。
  • 寄存器寻址,就是指令中有一个操作数是寄存器,例如 movl $666, %eax,其中%eax就是寄存器,这个内存寻址没有关系,也算是一种寻址,在汇编语言中使用助记符来表示,在机器指令中用几个bit表示寄存器的编号,这几个bit也可以看作寄存器的地址,但和内存地址不在同一个空间。

ELF文件

ELF文件是一个开放标准,各种Unix可执行文件都采用ELF格式,他又三种不同类型:

  • 可重定位文件(Relocatable,或者Object File)
  • 可执行文件(Executable)
  • 共享库(Shared Object,或者Shared Library)

共享库先不解释,先通过分析前边的寻找最大值程序来理解可重定位文件和可执行文件。

下面先详细叙述一下汇编、链接、执行的过程:

1.将汇编程序max.s文本文件。
2.汇编器读取文本并将其转化为max.o文件,通过汇编程序中的汇编指示进行相应操作,例如会根据.section将文件分为几个section,同时会自动添加一些section。
3.然后链接器将几个section组成几个segment,生成可执行文件max
4.最后加载器(loader)根据segment信息加载到相应内存位置中,之后运行程序。

可以看出可以将文件看成section或者segment的组合,在链接器中将其当作section的组合,在加载器中看作segment的集合。如图:
在这里插入图片描述
左边是链接器的视角,其中 ELF header 描述了体系结构和操作系统的基本信息,并指出 Section header table 和 Program header table 在什么位置,在链接时由于用不到 Program header table 所以会 Optional Ignored ,其中 Section header table 记录了section的信息包括位置。
同理在加载器的视角下,section header table 也会 optional ignored ,相应的 segment 位置记录在program header table 中。
图中可以看出一个segment由一个或者多个section构成,这是由于他们具有相同的访问权限,在最后加载时回家再相同的页中。有些section只对链接器有意义,并不需要加载到内存中,所以不属于任何segment。
这里要指出Section Header Table和Program Header Table并不是一定要位于文件的开头和结尾,而是由ELF header记录的地址。
目标文件需要做进一步处理,所以一定有section header table,而可执行文件由于要加载,所以一定有segment header table,而共享库既要加载运行,又要在加载时做动态链接,所以既有 Section Header Table 又有 Program Header Table。

目标文件

使用 readelf 工具读出目标文件 max.o 的 elf header 和 section header table:

$	readelf -a max.o
ELF Header:
Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          504 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         8
  Section header string table index: 7

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000002d  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000190
       0000000000000030  0000000000000018   I       5     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000006d
       0000000000000038  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  000000a5
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .symtab           SYMTAB           0000000000000000  000000a8
       00000000000000c0  0000000000000018           6     7     8
  [ 6] .strtab           STRTAB           0000000000000000  00000168
       0000000000000028  0000000000000000           0     0     1
  [ 7] .shstrtab         STRTAB           0000000000000000  000001c0
       0000000000000031  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

Relocation section '.rela.text' at offset 0x190 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000009  00020000000a R_X86_64_32       0000000000000000 .data + 0
00000000001a  00020000000a R_X86_64_32       0000000000000000 .data + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not 
currently supported.

Symbol table '.symtab' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     4: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    3 data_items
     5: 000000000000000f     0 NOTYPE  LOCAL  DEFAULT    1 start_loop
     6: 0000000000000026     0 NOTYPE  LOCAL  DEFAULT    1 loop_exit
     7: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    1 _start

No version information found in this file.

其中 ELF Header 中有如下描述:

  Start of section headers:          504 (bytes into file)
  
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         8
  Section header string table index: 7

上边一行记录了section header的位置,从文件地址的504开始(0x1F8),文件地址是这样定义的:文件开头第一个字节的地址是0,然后每个字节占一个地址。
下面两行表示没有 program header,之后两行行表示有8个 section header,每个section header 占64个字节。

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000002d  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000190
       0000000000000030  0000000000000018   I       5     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000006d
       0000000000000038  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  000000a5
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .symtab           SYMTAB           0000000000000000  000000a8
       00000000000000c0  0000000000000018           6     7     8
  [ 6] .strtab           STRTAB           0000000000000000  00000168
       0000000000000028  0000000000000000           0     0     1
  [ 7] .shstrtab         STRTAB           0000000000000000  000001c0
       0000000000000031  0000000000000000           0     0     1

从 section header 中可以看到我们在程序中声明的 .data 和 .text ,而其他的section都是汇编器自动添加的。Address 是这些段加载到内存中的地址,加载地址要在链接时添加的,所以现在地址是0,全部都是空缺的。offset 和 size 分别表示section的文件地址和大小,比如 .data size是0x38也就是14个4字节,恰好我们定义数组时也是14个元素,因此根据以上信息可以大致描绘出整个文件的布局。

起始文件地址section 或 header
0ELF Header
0x40.text
0x190.rela.text
0x6d.data
0xa5.bss
0xa8.symtab
0x168.strtab
0x1F8section header
0x1c0.shstrtab

我们可以使用 hexdump 工具把目标文件的自己全部打印出来:

00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  f8 01 00 00 00 00 00 00  |................|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 08 00 07 00  |....@.....@.....|
00000040  bf 00 00 00 00 67 8b 04  bd 00 00 00 00 89 c3 83  |.....g..........|
00000050  f8 00 74 12 ff c7 67 8b  04 bd 00 00 00 00 39 d8  |..t...g.......9.|
00000060  7e ed 89 c3 eb e9 b8 01  00 00 00 cd 80 03 00 00  |~...............|
00000070  00 43 00 00 00 22 00 00  00 de 00 00 00 2d 00 00  |.C...".......-..|
00000080  00 4b 00 00 00 36 00 00  00 22 00 00 00 2c 00 00  |.K...6..."...,..|
00000090  00 21 00 00 00 16 00 00  00 0b 00 00 00 42 00 00  |.!...........B..|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000000c0  00 00 00 00 03 00 01 00  00 00 00 00 00 00 00 00  |................|
000000d0  00 00 00 00 00 00 00 00  00 00 00 00 03 00 03 00  |................|
000000e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000f0  00 00 00 00 03 00 04 00  00 00 00 00 00 00 00 00  |................|
00000100  00 00 00 00 00 00 00 00  01 00 00 00 00 00 03 00  |................|
00000110  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000120  0c 00 00 00 00 00 01 00  0f 00 00 00 00 00 00 00  |................|
00000130  00 00 00 00 00 00 00 00  17 00 00 00 00 00 01 00  |................|
00000140  26 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |&...............|
00000150  21 00 00 00 10 00 01 00  00 00 00 00 00 00 00 00  |!...............|
00000160  00 00 00 00 00 00 00 00  00 64 61 74 61 5f 69 74  |.........data_it|
00000170  65 6d 73 00 73 74 61 72  74 5f 6c 6f 6f 70 00 6c  |ems.start_loop.l|
00000180  6f 6f 70 5f 65 78 69 74  00 5f 73 74 61 72 74 00  |oop_exit._start.|
00000190  09 00 00 00 00 00 00 00  0a 00 00 00 02 00 00 00  |................|
000001a0  00 00 00 00 00 00 00 00  1a 00 00 00 00 00 00 00  |................|
000001b0  0a 00 00 00 02 00 00 00  00 00 00 00 00 00 00 00  |................|
000001c0  00 2e 73 79 6d 74 61 62  00 2e 73 74 72 74 61 62  |..symtab..strtab|
000001d0  00 2e 73 68 73 74 72 74  61 62 00 2e 72 65 6c 61  |..shstrtab..rela|
000001e0  2e 74 65 78 74 00 2e 64  61 74 61 00 2e 62 73 73  |.text..data..bss|
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000230  00 00 00 00 00 00 00 00  20 00 00 00 01 00 00 00  |........ .......|
00000240  06 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000250  40 00 00 00 00 00 00 00  2d 00 00 00 00 00 00 00  |@.......-.......|
00000260  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00000270  00 00 00 00 00 00 00 00  1b 00 00 00 04 00 00 00  |................|
00000280  40 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |@...............|
00000290  90 01 00 00 00 00 00 00  30 00 00 00 00 00 00 00  |........0.......|
000002a0  05 00 00 00 01 00 00 00  08 00 00 00 00 00 00 00  |................|
000002b0  18 00 00 00 00 00 00 00  26 00 00 00 01 00 00 00  |........&.......|
000002c0  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000002d0  6d 00 00 00 00 00 00 00  38 00 00 00 00 00 00 00  |m.......8.......|
000002e0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
000002f0  00 00 00 00 00 00 00 00  2c 00 00 00 08 00 00 00  |........,.......|
00000300  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000310  a5 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000320  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00000330  00 00 00 00 00 00 00 00  01 00 00 00 02 00 00 00  |................|
00000340  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000350  a8 00 00 00 00 00 00 00  c0 00 00 00 00 00 00 00  |................|
00000360  06 00 00 00 07 00 00 00  08 00 00 00 00 00 00 00  |................|
00000370  18 00 00 00 00 00 00 00  09 00 00 00 03 00 00 00  |................|
00000380  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000390  68 01 00 00 00 00 00 00  28 00 00 00 00 00 00 00  |h.......(.......|
000003a0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
000003b0  00 00 00 00 00 00 00 00  11 00 00 00 03 00 00 00  |................|
000003c0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000003d0  c0 01 00 00 00 00 00 00  31 00 00 00 00 00 00 00  |........1.......|
000003e0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
000003f0  00 00 00 00 00 00 00 00  0a                       |.........|
000003f9

左边一列是相应数据所在的文件地址,中间是各个地址内数据的16进制表示,右侧是将相应数据按ASCII对应的字符表示出来,中间*部分表示省略的部分全是0。

根据section header 可以找到.data对应的是 0x6d到0xa5.

00000060  7e ed 89 c3 eb e9 b8 01  00 00 00 cd 80 03 00 00  |~...............|
00000070  00 43 00 00 00 22 00 00  00 de 00 00 00 2d 00 00  |.C...".......-..|
00000080  00 4b 00 00 00 36 00 00  00 22 00 00 00 2c 00 00  |.K...6..."...,..|
00000090  00 21 00 00 00 16 00 00  00 0b 00 00 00 42 00 00  |.!...........B..|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

.data段将被原封不动的加载到内存中,下面是.shstrtab.strtab这两个Section中存放的都是ASCII码:

.shstrtab:

000001c0  00 2e 73 79 6d 74 61 62  00 2e 73 74 72 74 61 62  |..symtab..strtab|
000001d0  00 2e 73 68 73 74 72 74  61 62 00 2e 72 65 6c 61  |..shstrtab..rela|
000001e0  2e 74 65 78 74 00 2e 64  61 74 61 00 2e 62 73 73  |.text..data..bss|

.strtab:

00000160  00 00 00 00 00 00 00 00  00 64 61 74 61 5f 69 74  |.........data_it|
00000170  65 6d 73 00 73 74 61 72  74 5f 6c 6f 6f 70 00 6c  |ems.start_loop.l|
00000180  6f 6f 70 5f 65 78 69 74  00 5f 73 74 61 72 74 00  |oop_exit._start.|

可以看出.shstrtab段记录的是各section的名字,.strtab存储的是程序中的符号,都以‘/0’为结束符。

C语言中对于全局变量如果没有复制的话,就会在程序加载的时候对其用0初始化。这种数据属于.bss段,在加载时它和.data段都是可读可写的数据,但是在ELF文件中.data段需要占一部分空间存储初始值,而bss段则不需要,也就是说.bss段有section header 但是没有对应的section,程序加载时.bss段占大多数内存在section header 中描述。

接下来看readelf输出的最后一部分,是从 .symtab.rel.text 中读出的信息。

Relocation section '.rela.text' at offset 0x190 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000009  00020000000a R_X86_64_32       0000000000000000 .data + 0
00000000001a  00020000000a R_X86_64_32       0000000000000000 .data + 0

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not 
currently supported.

Symbol table '.symtab' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     4: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    3 data_items
     5: 000000000000000f     0 NOTYPE  LOCAL  DEFAULT    1 start_loop
     6: 0000000000000026     0 NOTYPE  LOCAL  DEFAULT    1 loop_exit
     7: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT    1 _start

No version information found in this file.

.rela.text告诉链接器指令中那些地方需要做重定向。
.symtab是符号表,其中Nds是符号所在section的编号,比如567对应的符号都在1号section .text中,相应的section编号可以在 section header 中找到。其中 value 代表符号的地址,这里的地址是符号相对相应section的地址,例如 _start 就对应的 .text 的首地址,同时还可以看到在 bind 一栏中的 global 这是由于 _start 是用 .globl 声明的,而其他的符号都是local。

下面来看一看 text 段,使用 objdump 工具可以把程序中的机器指令反汇编(Disassemble),那么反汇编的结果是否跟原来写的汇编代码一模一样呢?我们对比分析一下。

$ objdump -d max.o

max.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_start>:
   0:	bf 00 00 00 00       	mov    $0x0,%edi
   5:	67 8b 04 bd 00 00 00 	mov    0x0(,%edi,4),%eax
   c:	00 
   d:	89 c3                	mov    %eax,%ebx

000000000000000f <start_loop>:
   f:	83 f8 00             	cmp    $0x0,%eax
  12:	74 12                	je     26 <loop_exit>
  14:	ff c7                	inc    %edi
  16:	67 8b 04 bd 00 00 00 	mov    0x0(,%edi,4),%eax
  1d:	00 
  1e:	39 d8                	cmp    %ebx,%eax
  20:	7e ed                	jle    f <start_loop>
  22:	89 c3                	mov    %eax,%ebx
  24:	eb e9                	jmp    f <start_loop>

0000000000000026 <loop_exit>:
  26:	b8 01 00 00 00       	mov    $0x1,%eax
  2b:	cd 80                	int    $0x80

左边是机器语言右边是反汇编之后的结果,显然相应的符号被替换成了对应的地址,这里的地址都是指的相对地址,例如把 loop_exit 替换成了26,没有加$的表示地址而不是数,对于符号 start_loop 等并不在包含在机器指令中,只是反汇编工具为了增加可读性,去查找symtab之后添加上的。
下一步,链接器需要把地址改成加载到内存中的地址,程序才能执行。

可执行文件

使用 readelf 工具观察可执行文件,看看和重定向文件相比做了什么改动。

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4000b0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          648 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         2
  Size of section headers:           64 (bytes)
  Number of section headers:         6
  Section header string table index: 5

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         00000000004000b0  000000b0
       000000000000002d  0000000000000000  AX       0     0     1
  [ 2] .data             PROGBITS         00000000006000dd  000000dd
       0000000000000038  0000000000000000  WA       0     0     1
  [ 3] .symtab           SYMTAB           0000000000000000  00000118
       0000000000000108  0000000000000018           4     7     8
  [ 4] .strtab           STRTAB           0000000000000000  00000220
       000000000000003f  0000000000000000           0     0     1
  [ 5] .shstrtab         STRTAB           0000000000000000  0000025f
       0000000000000027  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

There are no section groups in this file.

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000000dd 0x00000000000000dd  R E    0x200000
  LOAD           0x00000000000000dd 0x00000000006000dd 0x00000000006000dd
                 0x0000000000000038 0x0000000000000038  RW     0x200000

 Section to Segment mapping:
  Segment Sections...
   00     .text 
   01     .data 

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Advanced Micro Devices X86-64 is not 
currently supported.

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000004000b0     0 SECTION LOCAL  DEFAULT    1 
     2: 00000000006000dd     0 SECTION LOCAL  DEFAULT    2 
     3: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS max.o
     4: 00000000006000dd     0 NOTYPE  LOCAL  DEFAULT    2 data_items
     5: 00000000004000bf     0 NOTYPE  LOCAL  DEFAULT    1 start_loop
     6: 00000000004000d6     0 NOTYPE  LOCAL  DEFAULT    1 loop_exit
     7: 00000000004000b0     0 NOTYPE  GLOBAL DEFAULT    1 _start
     8: 0000000000600115     0 NOTYPE  GLOBAL DEFAULT    2 __bss_start
     9: 0000000000600115     0 NOTYPE  GLOBAL DEFAULT    2 _edata
    10: 0000000000600118     0 NOTYPE  GLOBAL DEFAULT    2 _end

No version information found in this file.

首先是文件类型 type 发生了改变,之前是 REL 现在变成了 EXEC ,有目标文件变成了可执行文件, Entry point address 由0变成了0x4000b0(这只_start的地址),还可以看出,多了两个program header ,少了两个 section header 。其中的两个 section text 和 data 的地址改成了 00000000006000dd 和 00000000004000b0。.bss 没有用到所以被删掉了,由于 .rel.text 段就是用于链接,所以在可执行文件中就删掉了。

多出了两个 program header , 这个是用来记录 segment 的信息的。第一个segment由.text段和前面的ELF Header、Program Header Table一起组成一个Segment(FileSiz指出总长度是0xdd),virtaddr列指出的是segment加载的首地址(虚拟地址),如第一个是0x0000000000400000,需注意的是在x86平台上后面的PhysAddr列是没有意义的,并不代表实际的物理地址。flg 列表明的是segment 的可读可执行权限,相应的字母含义在 Key to Flags 部分有说明,最后一列Align的值0x2000(8K)是x86平台的内存页面大小。在加载时文件也要按内存页面大小分成若干页,文件中的一页对应内存中的一页,对应关系如下图所示:

在这里插入图片描述

这个可执行文件很小,加起来都不一定有一页的大小,但是两个segment要放在不同的页中,这是因为MMU只能为一个页设置一种权限,而两个segment的权限不同。
此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然要偏移多少,同时我们可以在途中看到,.data的地址相对于第二页首地址的偏移量就是相对于header的偏移量,这是为了方便链接器和加载器的操作。
此外还多了三个符号__bss_start、_edata和_end,这些变量在链接脚本中定义,被链接器添加到可执行文件中,后续会介绍连接脚本。

接下来看一下反汇编的结果:

max:     file format elf64-x86-64


Disassembly of section .text:

00000000004000b0 <_start>:
  4000b0:	bf 00 00 00 00       	mov    $0x0,%edi
  4000b5:	67 8b 04 bd dd 00 60 	mov    0x6000dd(,%edi,4),%eax
  4000bc:	00 
  4000bd:	89 c3                	mov    %eax,%ebx

00000000004000bf <start_loop>:
  4000bf:	83 f8 00             	cmp    $0x0,%eax
  4000c2:	74 12                	je     4000d6 <loop_exit>
  4000c4:	ff c7                	inc    %edi
  4000c6:	67 8b 04 bd dd 00 60 	mov    0x6000dd(,%edi,4),%eax
  4000cd:	00 
  4000ce:	39 d8                	cmp    %ebx,%eax
  4000d0:	7e ed                	jle    4000bf <start_loop>
  4000d2:	89 c3                	mov    %eax,%ebx
  4000d4:	eb e9                	jmp    4000bf <start_loop>

00000000004000d6 <loop_exit>:
  4000d6:	b8 01 00 00 00       	mov    $0x1,%eax
  4000db:	cd 80                	int    $0x80

和之前的相比相对地址都替换为了绝对地址。

接下来是跳转命令,在跳转指令中只是改变了地址,并没有改变指令,这是因为跳转指令是相对于当前位置,进行跳转多少字节,而不是指定一个完整地址,内存中由32位,这些跳转指令只有16位,显然不一定能指定相应的地址,这称为相对跳转,这种指令只有16位,不能跳的太远,也有跳转指令可以跳到指定的地址,可以调到任何地方,这种叫做绝对跳转,后续会有例子。

最后再看内存访问指令,原来是这样的:

 5:	67 8b 04 bd 00 00 00 	mov    0x0(,%edi,4),%eax
 16:	67 8b 04 bd 00 00 00 	mov    0x0(,%edi,4),%eax

现在改成了:

4000b5:	67 8b 04 bd dd 00 60 	mov    0x6000dd(,%edi,4),%eax
4000c6:	67 8b 04 bd dd 00 60 	mov    0x6000dd(,%edi,4),%eax

原本是0x0地址,现在改成了0x6000dd(小端字节序),这里的改变是链接器根据.rel.text段来进行重定位的,信息如下:

Relocation section '.rela.text' at offset 0x190 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000009  00020000000a R_X86_64_32       0000000000000000 .data + 0
00000000001a  00020000000a R_X86_64_32       0000000000000000 .data + 0

根据第一列的offset可以找到要修改的位置的文件地址,相应的位置正式00 00 00 00对应的位置。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值