- 要彻底搞清楚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通用寄存器有eax
、ebx
、ecx
、edx
、edi
、esi
。对于通用寄存器大多数指令是可以任意使用的,比如movl指令可以移动到eax
也可以移动到ebx
。但对于一些特殊的指令只能使用其中几个通用寄存器,例如除法指令idivl
要求被除数在eax
中,edx
必须是0,而除数可以在任意寄存器中,因此对于特殊指令通用寄存器并不通用。
除通用寄存器外还有特殊寄存器,ebp
、esp
、eip
、eflags
。eip
是程序计数器,eflag
用于记录运算过程中的标志位,如进位标志,负数标志,以及零标志。ebp
、esp
用于维护函数调用栈帧。
第二个汇编程序
求一组数的最大值:
#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 |
---|---|
0 | ELF Header |
0x40 | .text |
0x190 | .rela.text |
0x6d | .data |
0xa5 | .bss |
0xa8 | .symtab |
0x168 | .strtab |
0x1F8 | section 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对应的位置。