龙芯Mips平台vmlinux文件分析

一、文件格式

首先,vmlinux 属于 ELF(Executable and Linkable Format) ⽂件,要想了解如何启动 vmlinux,⾸先需要知道 ELF 的格式。
ELF 格式

  • text段
    代码段,通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定。

  • data段
    数据段,通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

  • bss段
    通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态内存分配。

  • init段
    linux定义的一种初始化过程中才会用到的段,一旦初始化完成,那么这些段所占用的内存会被释放掉,后续会继续说明。

1. 文件类型

在 Linux 系统中,一个 ELF 文件主要用来表示 3 种类型的文件:

  1. 可执行文件(EXEC)
  2. 目标文件(.o)
  3. 共享文件(.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 种不同的场合下被使用:

  1. 可执行文件:被操作系统中的加载器从硬盘上读取,载入到内存中去执行;
  2. 目标文件:被链接器读取,用来产生一个可执行文件或者共享库文件;
  3. 共享库文件:在动态链接的时候,由 ld-linux.so 来读取;

对于链接器而言,关心 ELF header, Sections 以及 Section header table 这 3 部分内容:
链接器
对于加载器只关心 ELF header, Program header tableSegment 这 3 部分内容。
加载器
对于中间部分的 Sections, 它改了个名字,叫做 Segments(段)。换汤不换药,本质上都是一样的。
可以理解为:一个 Segment 可能包含一个或者多个 Sections,就像下面这样:
在这里插入图片描述
这就好比超市里的货架上摆放的商品:有矿泉水、可乐、啤酒,巧克力,牛肉干,薯片。

从理货员的角度看:它们属于 6 种不同的商品;但是从超市经理的角度看,它们只属于 2 类商品:饮料和零食。

二、结构内容描述

在 Linux 系统中,会有不同的数据结构来描述上面所说的每部分内容,该文件为:usr/include/elf.h

  • 描述 ELF header 的结构体:

    #define EI_NIDENT (16)
    
    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 的结构体:

    /* Program segment header.  */
    
    typedef struct
    {
      Elf64_Word	p_type;			/* Segment type */
      Elf64_Word	p_flags;		/* Segment flags */
      Elf64_Off	p_offset;		/* Segment file offset */
      Elf64_Addr	p_vaddr;		/* Segment virtual address */
      Elf64_Addr	p_paddr;		/* Segment physical address */
      Elf64_Xword	p_filesz;		/* Segment size in file */
      Elf64_Xword	p_memsz;		/* Segment size in memory */
      Elf64_Xword	p_align;		/* Segment alignment */
    } Elf64_Phdr;
    
  • 描述 Section header table 的结构体:

    /* Section header.  */
    
    typedef struct
    {
      Elf64_Word	sh_name;		/* Section name (string tbl index) */
      Elf64_Word	sh_type;		/* Section type */
      Elf64_Xword	sh_flags;		/* Section flags */
      Elf64_Addr	sh_addr;		/* Section virtual addr at execution */
      Elf64_Off	sh_offset;		/* Section file offset */
      Elf64_Xword	sh_size;		/* Section size in bytes */
      Elf64_Word	sh_link;		/* Link to another section */
      Elf64_Word	sh_info;		/* Additional section information */
      Elf64_Xword	sh_addralign;		/* Section alignment */
      Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
    } 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 个字节,具体含义如下(数据内容可能不对,主要看含义):
0-15字节

官方文档对于这部分的解释是:
在这里插入图片描述
关于大端、小端格式,这个vmlinux 文件中显示的是 1,代表mips平台是小端格式。
关于大小端模式可以看下面的图片

  • 大端模式 在这里插入图片描述

  • 小端模式
    在这里插入图片描述
    剩下的 48 个字节(64 - 16 = 48),的字节码含义:

  • 16 - 31 字节:
    在这里插入图片描述

  • 32 - 47 字节:
    32 - 47 字节

  • 48 - 63 个字节:
    在这里插入图片描述

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 结构体中的每个成员变量:

/* Section header.  */

typedef struct
{
  Elf64_Word	sh_name;		/* Section name (string tbl index) */
  Elf64_Word	sh_type;		/* Section type */
  Elf64_Xword	sh_flags;		/* Section flags */
  Elf64_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf64_Off	sh_offset;		/* Section file offset */
  Elf64_Xword	sh_size;		/* Section size in bytes */
  Elf64_Word	sh_link;		/* Link to another section */
  Elf64_Word	sh_info;		/* Additional section information */
  Elf64_Xword	sh_addralign;		/* Section alignment */
  Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
} 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;		/* Section name (string tbl index) */
  Elf64_Word	sh_type;		/* Section type */
  Elf64_Xword	sh_flags;		/* Section flags */
  Elf64_Addr	sh_addr;		/* Section virtual addr at execution */
  Elf64_Off	sh_offset;		/* Section file offset */
  Elf64_Xword	sh_size;		/* Section size in bytes */
  Elf64_Word	sh_link;		/* Link to another section */
  Elf64_Word	sh_info;		/* Additional section information */
  Elf64_Xword	sh_addralign;		/* Section alignment */
  Elf64_Xword	sh_entsize;		/* Entry size if section holds table */
} 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

显示的信息已经很明白了:

  1. 这是一个可执行程序;
  2. 入口地址是 0xffffffff81b9b740;
  3. 一共有 2 个 Program header,是从 ELF 文件的 64 个偏移地址开始的;
  4. 此段对应的 section 为.head.text .text .got.plt…,所以 vmlinux 的入口在 .head.text 文本段。

三、.head.text 文本段

通过 vmlinux.lds.S 找到 vmlinux 的入口函数。具体分析如下:

/* SPDX-License-Identifier: GPL-2.0 */
#include <asm/asm-offsets.h>
#include <asm/thread_info.h>

#define PAGE_SIZE _PAGE_SIZE

/*
 * Put .bss..swapper_pg_dir as the first thing in .bss. This will
 * ensure that it has .bss alignment (64K).
 */
#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 文件:扒开它的层层外衣,从字节码的粒度来探索
一文搞懂 | 内核的启动

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值