一、文件格式
首先,vmlinux 属于 ELF(Executable and Linkable Format) ?件,要想了解如何启动 vmlinux,?先需要知道 ELF 的格式。
-
text段 代码段,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定。 -
data段 数据段,通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。 -
bss段 通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态内存分配。 -
init段 linux定义的一种初始化过程中才会用到的段,一旦初始化完成,那么这些段所占用的内存会被释放掉,后续会继续说明。
1. 文件类型
在 Linux 系统中,一个 ELF 文件主要用来表示 3 种类型的文件:
- 可执行文件(EXEC)
- 目标文件(.o)
- 共享文件(.so)
在vmlinux 文件头部内容中,就存在一个字段,用来表示:当前这个 ELF 文件,它到底是一个什么类型的文件,可以通过readelf -h vmlinux 命令查看到mips平台上的这个vmlinux文件是一个可执行文件(EXEC)
user$ readelf -h vmlinux
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: MIPS R3000
版本: 0x1
入口点地址: 0xffffffff81b9b740
程序头起点: 64 (bytes into file)
Start of section headers: 258099400 (bytes into file)
标志: 0x80000001, noreorder, mips64r2
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 2
节头大小: 64 (字节)
节头数量: 34
字符串表索引节头: 33
2. 使用场景
既然vmlinux 文件有3 种类型,那么肯定是在 3 种不同的场合下被使用:
- 可执行文件:被操作系统中的加载器从硬盘上读取,载入到内存中去执行;
- 目标文件:被链接器读取,用来产生一个可执行文件或者共享库文件;
- 共享库文件:在动态链接的时候,由 ld-linux.so 来读取;
对于链接器而言,关心 ELF header , Sections 以及 Section header table 这 3 部分内容: 对于加载器只关心 ELF header , Program header table 和 Segment 这 3 部分内容。 对于中间部分的 Sections, 它改了个名字,叫做 Segments(段)。换汤不换药,本质上都是一样的。 可以理解为:一个 Segment 可能包含一个或者多个 Sections,就像下面这样: 这就好比超市里的货架上摆放的商品:有矿泉水、可乐、啤酒,巧克力,牛肉干,薯片。
从理货员的角度看:它们属于 6 种不同的商品;但是从超市经理的角度看,它们只属于 2 类商品:饮料和零食。
二、结构内容描述
在 Linux 系统中,会有不同的数据结构来描述上面所说的每部分内容,该文件为:usr/include/elf.h 。
-
描述 ELF header 的结构体:
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file version */
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
-
描述 Program header table 的结构体:
typedef struct
{
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
Elf64_Xword p_filesz;
Elf64_Xword p_memsz;
Elf64_Xword p_align;
} Elf64_Phdr;
-
描述 Section header table 的结构体:
typedef struct
{
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
1. ELF header(ELF 头)
头部内容,就相当于是一个总管,它决定了这个完整的 ELF 文件内部的所有信息,比如:
这是一个 ELF 文件; 一些基本信息:版本,文件类型,机器类型; Program header table(程序头表)的开始地址,在整个文件的什么地方; Section header table(节头表)的开始地址,在整个文件的什么地方;
可以通过readelf -h vmlinux 命令查看到mips平台上的vmlinux的header信息
user$ readelf -h vmlinux
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: MIPS R3000
版本: 0x1
入口点地址: 0xffffffff81b9b740
程序头起点: 64 (bytes into file)
Start of section headers: 258099400 (bytes into file)
标志: 0x80000001, noreorder, mips64r2
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 2
节头大小: 64 (字节)
节头数量: 34
字符串表索引节头: 33
在一个 ELF 文件中,存在很多个 Sections/Segments ,这些 Sections 的具体信息,是在 Program header table 或者 Section head table 中进行描述的。
例如 Section head table :
假如一个 ELF 文件中一共存在 4 个 Section : .text 、.rodata 、.data 、.bss ,那么在 Section head table 中,将会有 4 个 Entry(条目)来分别描述这 4 个 Section 的具体信息(严格来说,不止 4 个 Entry,因为还存在一些其他辅助的 Sections),就像下面这样: 前面看到的vmlinux 文件ELF header 部分的内容,一共是 64 个字节,查看开头的这 64 个字节码。 用命令:od -Ax -t x1 -N 64 vmlinux 来读取vmlinux 中的字节码。
-Ax: 显示地址的时候,用十六进制来表示。如果使用 -Ad,意思就是用十进制来显示地址; -t -x1: 显示字节码内容的时候,使用十六进制(x),每次显示一个字节(1); -N 52:只需要读取 52 个字节;
user$ od -Ax -t x1 -N 64 vmlinux
000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
000010 02 00 08 00 01 00 00 00 40 b7 b9 81 ff ff ff ff
000020 40 00 00 00 00 00 00 00 c8 48 62 0f 00 00 00 00
000030 01 00 00 80 40 00 38 00 02 00 40 00 22 00 21 00
000040
这 64 个字节的内容,你可以对照上面的结构体中每个字段来解释了。 首先看一下前 16 个字节。 在结构体中的第一个成员是 unsigned char e_ident[EI_NIDENT] ,EI_NIDENT 的长度是 16,代表了 EL header 中的开始 16 个字节,具体含义如下(数据内容可能不对,主要看含义):
官方文档对于这部分的解释是: 关于大端、小端格式,这个vmlinux 文件中显示的是 1,代表mips平台是小端格式。 关于大小端模式可以看下面的图片
2. 字符串表表项 Entry
在一个 ELF 文件中,存在很多字符串,例如:变量名、Section名称、链接器加入的符号等等,这些字符串的长度都是不固定的,因此用一个固定的结构来表示这些字符串,肯定是不现实的。
于是:把这些字符串集中起来,统一放在一起,作为一个独立的 Section 来进行管理。
在文件中的其他地方呢,如果想表示一个字符串,就在这个地方写一个数字索引:表示这个字符串位于字符串统一存储地方的某个偏移位置,经过这样的按图索骥,就可以找到这个具体的字符串了。
比如说啊,下面这个空间中存储了所有的字符串: 在程序的其他地方,如果想引用字符串 “hello,world!”,那么就只需要在那个地方标明数字 13 就可以了,表示:这个字符串从偏移 13 个字节处开始。
在 ELF header 的最后 2 个字节是 0x21 0x00 ,它对应结构体中的成员 e_shstrndx ,意思是这个 ELF 文件中,字符串表是一个普通的 Section ,在这个 Section 中,存储了 ELF 文件中使用到的所有的字符串。
既然是一个 Section ,那么在 Section header table 中,就一定有一个表项 Entry 来描述它,那么是哪一个表项呢?
这就是 0x21 0x00 这个表项,也就是第 33 个表项。 这里,我们还可以用指令 readelf -S vmlinux 来看一下这个 ELF 文件中所有的 Section 信息:
uos$ readelf -S vmlinux
There are 34 section headers, starting at offset 0xf6248c8:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS ffffffff80200000 00004000
00000000019b8524 0000000000000000 AX 0 0 64
[ 2] __ex_table PROGBITS ffffffff81bb8530 019bc530
0000000000006400 0000000000000000 A 0 0 8
[ 3] .notes NOTE ffffffff81bbe930 019c2930
0000000000000054 0000000000000000 A 0 0 4
[ 4] .rodata PROGBITS ffffffff81bbf000 019c3000
000000000032ec50 0000000000000000 WA 0 0 256
[ 5] .pci_fixup PROGBITS ffffffff81eedc50 01cf1c50
00000000000031c8 0000000000000000 A 0 0 8
[ 6] __ksymtab PROGBITS ffffffff81ef0e18 01cf4e18
00000000000161b0 0000000000000000 A 0 0 8
[ 7] __ksymtab_gpl PROGBITS ffffffff81f06fc8 01d0afc8
0000000000012250 0000000000000000 A 0 0 8
[ 8] __kcrctab PROGBITS ffffffff81f19218 01d1d218
000000000000586c 0000000000000000 A 0 0 4
[ 9] __kcrctab_gpl PROGBITS ffffffff81f1ea84 01d22a84
0000000000004894 0000000000000000 A 0 0 4
[10] __ksymtab_strings PROGBITS ffffffff81f23318 01d27318
00000000000318ab 0000000000000000 A 0 0 1
[11] __param PROGBITS ffffffff81f54bc8 01d58bc8
00000000000046c8 0000000000000000 A 0 0 8
[12] __modver PROGBITS ffffffff81f59290 01d5d290
0000000000000d70 0000000000000000 A 0 0 8
[13] .data PROGBITS ffffffff81f5a000 01d5e000
00000000000a7160 0000000000000000 WA 0 0 64
[14] .data..page_align PROGBITS ffffffff82004000 01e08000
0000000000010000 0000000000000000 WA 0 0 16384
[15] .init.text PROGBITS ffffffff82014000 01e18000
0000000000081f40 0000000000000000 AX 0 0 64
[16] .init.data PROGBITS ffffffff82096000 01e9a000
000000000001e880 0000000000000000 WA 0 0 256
[17] .exit.text PROGBITS ffffffff820b4880 01eb8880
0000000000003e40 0000000000000000 AX 0 0 64
[18] .data..percpu PROGBITS ffffffff820bc000 01ec0000
000000000000b560 0000000000000000 WA 0 0 64
[19] .bss NOBITS ffffffff820d0000 01ecb560
000000000114d5e0 0000000000000000 WA 0 0 16384
[20] .mdebug.abi64 PROGBITS ffffffff8321d5e0 01ecb560
0000000000000000 0000000000000000 0 0 1
[21] .comment PROGBITS 0000000000000000 01ecb560
000000000000002f 0000000000000001 MS 0 0 1
[22] .gnu.attributes GNU_ATTRIBUTES 0000000000000000 01ecb58f
0000000000000010 0000000000000000 0 0 1
[23] .debug_aranges MIPS_DWARF 0000000000000000 01ecb5a0
00000000000241f0 0000000000000000 0 0 16
[24] .debug_info MIPS_DWARF 0000000000000000 01eef790
000000000abb1ae8 0000000000000000 0 0 1
[25] .debug_abbrev MIPS_DWARF 0000000000000000 0caa1278
00000000004a58ca 0000000000000000 0 0 1
[26] .debug_line MIPS_DWARF 0000000000000000 0cf46b42
0000000000ae322e 0000000000000000 0 0 1
[27] .debug_frame MIPS_DWARF 0000000000000000 0da29d70
0000000000228770 0000000000000000 0 0 8
[28] .debug_str MIPS_DWARF 0000000000000000 0dc524e0
00000000002dfb06 0000000000000001 MS 0 0 1
[29] .debug_loc MIPS_DWARF 0000000000000000 0df31fe6
0000000000d34671 0000000000000000 0 0 1
[30] .debug_ranges MIPS_DWARF 0000000000000000 0ec66660
00000000005ad8b0 0000000000000000 0 0 16
[31] .symtab SYMTAB 0000000000000000 0f213f10
000000000023aab0 0000000000000018 32 61785 8
[32] .strtab STRTAB 0000000000000000 0f44e9c0
00000000001d5d9a 0000000000000000 0 0 1
[33] .shstrtab STRTAB 0000000000000000 0f62475a
0000000000000168 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),
p (processor specific)
其中的第 33 个 Section,描述的正是字符串表 Section:
[33] .shstrtab STRTAB 0000000000000000 0f62475a
0000000000000168 0000000000000000 0 0 1
可以看出来:这个 Section 在 ELF 文件中的偏移地址是 0x0f62475a ,长度是 0x0000000000000168 个字节。
3. 读取字符串表 Section 的内容
下面,我们从 ELF header 的二进制数据中,来推断这信息。
要想打印字符串表 Section 的内容,就必须知道这个 Section 在 ELF 文件中的偏移地址。
要想知道偏移地址,只能从 Section head table 中第 33 个表项描述信息中获取。
要想知道第 33 个表项的地址,就必须知道 Section head table 在 ELF 文件中的开始地址,以及每一个表项的大小。
正好最后这 2 个需求信息,在 ELF header 中都告诉我们了。
ELF header 中的第 40 到 47 字节内容是:c8 48 62 0f 00 00 00 00 (注意这里的字节序,低位在前),表示的就是 Section head table 在 ELF 文件中的开始地址(e_shoff )。
0x0f6248c8 = 258099400 ,也就是说 Section head table 的开始地址位于 ELF 文件的第 258099400 个字节处。
知道了开始地址,再来算一下第 33 个表项 Entry 的地址。
ELF header 中的第 52、53 字节内容是:40 00 ,表示每个表项的长度是 0x0040 = 64 个字节。
注意这里的计算都是从 0 开始的,因此第 33 个表项的开始地址就是:258099400 + 33 * 64 = 258101512 ,也就是说用来描述字符串表这个 Section 的表项,位于 ELF 文件的 258101512 字节的位置。
既然知道了这个表项 Entry 的地址,那么就扒开来看一下其中的二进制内容:
执行指令:od -Ad -t x1 -j 258101512 -N 64 vmlinux 。
其中的 -j 258101512 选项,表示跳过前面的 258101512 个字节,也就是我们从 vmlinux 这个 ELF 文件的 258101512 字节处开始读取,一共读 64 个字节。
uos$ od -Ad -t x1 -j 258101512 -N 64 vmlinux
258101512 11 00 00 00 03 00 00 00 00 00 00 00 00 00 00 00
258101528 00 00 00 00 00 00 00 00 5a 47 62 0f 00 00 00 00
258101544 68 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00
258101560 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
258101576
这 64 个字节的内容,就对应了 Elf32_Shdr 结构体中的每个成员变量:
typedef struct
{
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
这里主要关注一下以下 4 个字段:
sh_name: ; sh_type:表示这个 Section 的类型,3 表示这是一个 string table;
sh_offset: 表示这个 Section,在 ELF 文件中的偏移量。0x0f62475a = 258099034,意思是字符串表这个 Section 的内容,从 ELF 文件的 258099034 个字节处开始;
sh_size:表示这个 Section 的长度。0x00000168 = 360 个字节,意思是字符串表这个 Section 的内容,一共有 360 个字节。
与之前使用 readelf 工具,读取到字符串表 Section 在 ELF 文件中的偏移地址是 0x0f62475a ,长度是 0x000168 个字节吗
[33] .shstrtab STRTAB 0000000000000000 0f62475a
0000000000000168 0000000000000000 0 0 1
既然知道了字符串表这个 Section 在 ELF 文件中的偏移量以及长度,那么执行指令: od -Ad -t c -j 258099034 -N 360 vmlinux ,就可以把它的字节码内容读取出来。
uos@uos-PC:/media/uos/data/project/mips-kernel/loongson-kernel$ od -Ad -t c -j 258099034 -N 360 vmlinux
258099034 \0 . s y m t a b \0 . s t r t a b
258099050 \0 . s h s t r t a b \0 _ _ e x _
258099066 t a b l e \0 . n o t e s \0 . r o
258099082 d a t a \0 . p c i _ f i x u p \0
258099098 _ _ k s y m t a b \0 _ _ k s y m
258099114 t a b _ g p l \0 _ _ k c r c t a
258099130 b \0 _ _ k c r c t a b _ g p l \0
258099146 _ _ k s y m t a b _ s t r i n g
258099162 s \0 _ _ p a r a m \0 _ _ m o d v
258099178 e r \0 . d a t a . . p a g e _ a
258099194 l i g n e d \0 . i n i t . t e x
258099210 t \0 . i n i t . d a t a \0 . e x
258099226 i t . t e x t \0 . d a t a . . p
258099242 e r c p u \0 . b s s \0 . m d e b
258099258 u g . a b i 6 4 \0 . c o m m e n
258099274 t \0 . g n u . a t t r i b u t e
258099290 s \0 . d e b u g _ a r a n g e s
258099306 \0 . d e b u g _ i n f o \0 . d e
258099322 b u g _ a b b r e v \0 . d e b u
258099338 g _ l i n e \0 . d e b u g _ f r
258099354 a m e \0 . d e b u g _ s t r \0 .
258099370 d e b u g _ l o c \0 . d e b u g
258099386 _ r a n g e s \0
258099394
这个 Section 中存储的全部是字符串
刚才没有解释 sh_name 这个字段,它表示字符串表这个 Section 本身的名字,既然是名字,那一定是个字符串。
但是这个字符串不是直接存储在这里的,而是存储了一个索引,索引值是 0x00000011 ,也就是十进制数值 17。
现在我们来数一下字符串表 Section 内容中,第 17 个字节开始的地方,存储的正是:“.shstrtab” 这个字符串(\0是字符串的分隔符)。
4. 读取代码段的内容
从下面的代码(指令:readelf -S vmlinux ):
user@$ readelf -S vmlinux
There are 34 section headers, starting at offset 0xf6248c8:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS ffffffff80200000 00004000
00000000019b8524 0000000000000000 AX 0 0 64
[ 2] __ex_table PROGBITS ffffffff81bb8530 019bc530
0000000000006400 0000000000000000 A 0 0 8
[ 3] .notes NOTE ffffffff81bbe930 019c2930
0000000000000054 0000000000000000 A 0 0 4
[ 4] .rodata PROGBITS ffffffff81bbf000 019c3000
000000000032ec50 0000000000000000 WA 0 0 256
[ 5] .pci_fixup PROGBITS ffffffff81eedc50 01cf1c50
00000000000031c8 0000000000000000 A 0 0 8
[ 6] __ksymtab PROGBITS ffffffff81ef0e18 01cf4e18
00000000000161b0 0000000000000000 A 0 0 8
[ 7] __ksymtab_gpl PROGBITS ffffffff81f06fc8 01d0afc8
0000000000012250 0000000000000000 A 0 0 8
[ 8] __kcrctab PROGBITS ffffffff81f19218 01d1d218
000000000000586c 0000000000000000 A 0 0 4
[ 9] __kcrctab_gpl PROGBITS ffffffff81f1ea84 01d22a84
0000000000004894 0000000000000000 A 0 0 4
[10] __ksymtab_strings PROGBITS ffffffff81f23318 01d27318
00000000000318ab 0000000000000000 A 0 0 1
[11] __param PROGBITS ffffffff81f54bc8 01d58bc8
00000000000046c8 0000000000000000 A 0 0 8
[12] __modver PROGBITS ffffffff81f59290 01d5d290
0000000000000d70 0000000000000000 A 0 0 8
[13] .data PROGBITS ffffffff81f5a000 01d5e000
00000000000a7160 0000000000000000 WA 0 0 64
[14] .data..page_align PROGBITS ffffffff82004000 01e08000
0000000000010000 0000000000000000 WA 0 0 16384
[15] .init.text PROGBITS ffffffff82014000 01e18000
0000000000081f40 0000000000000000 AX 0 0 64
[16] .init.data PROGBITS ffffffff82096000 01e9a000
000000000001e880 0000000000000000 WA 0 0 256
[17] .exit.text PROGBITS ffffffff820b4880 01eb8880
0000000000003e40 0000000000000000 AX 0 0 64
[18] .data..percpu PROGBITS ffffffff820bc000 01ec0000
000000000000b560 0000000000000000 WA 0 0 64
[19] .bss NOBITS ffffffff820d0000 01ecb560
000000000114d5e0 0000000000000000 WA 0 0 16384
[20] .mdebug.abi64 PROGBITS ffffffff8321d5e0 01ecb560
0000000000000000 0000000000000000 0 0 1
[21] .comment PROGBITS 0000000000000000 01ecb560
000000000000002f 0000000000000001 MS 0 0 1
[22] .gnu.attributes GNU_ATTRIBUTES 0000000000000000 01ecb58f
0000000000000010 0000000000000000 0 0 1
[23] .debug_aranges MIPS_DWARF 0000000000000000 01ecb5a0
00000000000241f0 0000000000000000 0 0 16
[24] .debug_info MIPS_DWARF 0000000000000000 01eef790
000000000abb1ae8 0000000000000000 0 0 1
[25] .debug_abbrev MIPS_DWARF 0000000000000000 0caa1278
00000000004a58ca 0000000000000000 0 0 1
[26] .debug_line MIPS_DWARF 0000000000000000 0cf46b42
0000000000ae322e 0000000000000000 0 0 1
[27] .debug_frame MIPS_DWARF 0000000000000000 0da29d70
0000000000228770 0000000000000000 0 0 8
[28] .debug_str MIPS_DWARF 0000000000000000 0dc524e0
00000000002dfb06 0000000000000001 MS 0 0 1
[29] .debug_loc MIPS_DWARF 0000000000000000 0df31fe6
0000000000d34671 0000000000000000 0 0 1
[30] .debug_ranges MIPS_DWARF 0000000000000000 0ec66660
00000000005ad8b0 0000000000000000 0 0 16
[31] .symtab SYMTAB 0000000000000000 0f213f10
000000000023aab0 0000000000000018 32 61785 8
[32] .strtab STRTAB 0000000000000000 0f44e9c0
00000000001d5d9a 0000000000000000 0 0 1
[33] .shstrtab STRTAB 0000000000000000 0f62475a
0000000000000168 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),
p (processor specific)
可以看到代码段是位于第 1 个表项中,加载(虚拟)地址是 ffffffff80200000 ,它位于 ELF 文件中的偏移量是 00004000 ,长度是 00000000019b8524 个字节。
首先计算这个表项 Entry 的地址:258099400 + 1 * 64 = 258099464 。
然后读取这个表项 Entry,读取指令是 od -Ad -t x1 -j 258099464 -N 64 vmlinux :
user@$ od -Ad -t x1 -j 258099464 -N 64 vmlinux
258099464 ac 00 00 00 01 00 00 00 06 00 00 00 00 00 00 00
258099480 00 00 20 80 ff ff ff ff 00 40 00 00 00 00 00 00
258099496 24 85 9b 01 00 00 00 00 00 00 00 00 00 00 00 00
258099512 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
258099528
typedef struct
{
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
同样的,我们也只关心下面这 5 个字段内容:
sh_name: 这回应该清楚了,表示代码段的名称在字符串表 Section 中的偏移位置。0xac = 172 字节,也就是在字符串表 Section 的第 172 字节处,存储的就是代码段的名字。回过头去找一下,看一下是不是字符串 “.text”;
sh_type:表示这个 Section 的类型,1(SHT_PROGBITS) 表示这是代码;
sh_addr:表示这个 Section 加载的虚拟地址是 0xffffffff80200000 ,这个值与 ELF header 中的 e_entry 字段的值是相同的;
sh_offset: 表示这个 Section,在 ELF 文件中的偏移量。0x00000040 = 64 ,意思是这个 Section 的内容,从 ELF 文件的 64 个字节处开始;
sh_size:表示这个 Section 的长度。0x00000000019b8524 = 26969380 个字节,意思是代码段一共有 26969380 个字节。
以上这些分析结构,与指令 readelf -S main 读取出来的完全一样!
PS: 在查看字符串表 Section 中的字符串时,可以计算一下:字符串表的开始地址是 258099034(十进制),加上 172,结果就是 258099206,所以从 258099206 开始的地方,就是代码段的名称,也就是 “.text”。
知道了以上这些信息,我们就可以读取代码段的字节码了.使用指令:od -Ad -t x1 -j 64 -N 26969380 vmlinux 即可。
5. Program header
文章的开头,链接器和加载器读取vmlinx的结果是不同的。
先用 readelf 这个工具来从总体上看一下 vmlinux 文件中的所有段信息。
执行指令:readelf -l vmlinux ,得到以下代码:
user$ readelf -l vmlinux
Elf 文件类型为 EXEC (可执行文件)
Entry point 0xffffffff81b9b740
There are 2 program headers, starting at offset 64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000004000 0xffffffff80200000 0xffffffff80200000
0x0000000001ec7560 0x000000000301d5e0 RWE 0x4000
NOTE 0x00000000019c2930 0xffffffff81bbe930 0xffffffff81bbe930
0x0000000000000054 0x0000000000000054 R 0x4
Section to Segment mapping:
段节...
00 .text __ex_table .notes .rodata .pci_fixup __ksymtab __ksymtab_gpl __kcrctab __kcrctab_gpl __ksymtab_strings __param __modver .data .data..page_aligned .init.text .init.data .exit.text .data..percpu .bss
01 .notes
显示的信息已经很明白了:
- 这是一个可执行程序;
- 入口地址是
0xffffffff81b9b740 ; - 一共有 2 个 Program header,是从 ELF 文件的 64 个偏移地址开始的;
- 此段对应的 section 为.head.text .text .got.plt…,所以 vmlinux 的入口在
.head.text 文本段。
三、.head.text 文本段
通过 vmlinux.lds.S 找到 vmlinux 的入口函数。具体分析如下:
#include <asm/asm-offsets.h>
#include <asm/thread_info.h>
#define PAGE_SIZE _PAGE_SIZE
#define BSS_FIRST_SECTIONS *(.bss..swapper_pg_dir)
#include <asm-generic/vmlinux.lds.h>
#undef mips
#define mips mips
OUTPUT_ARCH(mips)
ENTRY(kernel_entry)
根据链接脚本语法,可以知道 OUTPUT_ARCH 关键字指定了链接之后的输出文件的体系结构是 mips 。ENTRY 关键字指定了输出文件 vmlinux 的入口 地址是 kernel_entry , 因此只需找到 kernel_entry 的定义就可以知道 vmlinux 的入口函数。 参考我之前写过的基于Mips平台的Linux Kernel启动过程可知
内核的初始入口位于 arch/mips/kernel/head.S 中的 kernel_entry 。严格来说,kernel_entry 只是非压缩版原始内核的执行入口点(编译内核产生的 ELF可执行内核文件叫 vmlinux ,即非压缩版的原始内核;将 vmlinux 压缩以后再加上一个新的 ELF 头就得到压缩版内核 vmlinuz ;BIOS 既可以启动压缩版内核,也可以启动原始内核,龙芯平台启动的是压缩版内核)。
之后便是内核启动过程了。
参考链接
Linux 系统中编译、链接的基石 - ELF 文件:扒开它的层层外衣,从字节码的粒度来探索 一文搞懂 | 内核的启动
|