C 语言的编译

多文件的 C 语言代码是如何一同编译为同一个可执行文件的呢?

例如:

#include <stdio.h>
int main() {
  puts("Hello, World!");
  return 0;
}
  • 预处理

gcc -E <file>

//...
 
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__))
  __attribute__ ((__access__ (__write_only__, 1)));
# 941 "/usr/include/stdio.h" 3 4
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
 
 
 
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
 
 
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1)));
# 959 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 983 "/usr/include/stdio.h" 3 4
 
# 2 "test.c" 2
 
 
# 3 "test.c"
int main() {
  puts("Hello World!");
  return 0;
}

C 语言的预处理器将头文件的内容添加到了源文件中, 这一阶段处理后, 仍然只有头文件内函数的声明, 而没有定义.

  • 编译

gcc -g -c <file>: -g 是保留调试信息, -c 是只编译不链接.

反汇编: objdump --section=.text --disassemble=main --source <file>.o > <output>
其中--section=.text 表示仅处理 .text 节的内容; --disassemble=main 表示仅反汇编 main 符号的代码; --source 表示显示汇编代码与源代码的对应关系.

test.o:     文件格式 elf64-x86-64
 
 
Disassembly of section .text:
 
0000000000000000 <main>:
#include <stdio.h>
 
int main() {
   0: f3 0f 1e fa           endbr64
   4: 55                    push   %rbp
   5: 48 89 e5              mov    %rsp,%rbp
  puts("Hello World!");
   8: 48 8d 05 00 00 00 00  lea    0x0(%rip),%rax        # f <main+0xf>
   f: 48 89 c7              mov    %rax,%rdi
  12: e8 00 00 00 00        call   17 <main+0x17>
  return 0;
  17: b8 00 00 00 00        mov    $0x0,%eax
}
  1c: 5d                    pop    %rbp
  1d: c3                    ret

会发现本该填写调用外部函数的地址的位置上被填写了一串 0. 那个地址显然不可能是该函数的地址. 也就是说,直到这一步, 其具体实现依然不在我们的程序中。

  • 编译并链接

gcc -g -static [-o <output>] <file>:
-static 表示进行静态链接, 若不指定, 则现代的系统上可能默认使用动态链接.
使用静态链接时, 生成的二进制文件中即含有所有需要使用的函数的代码, 便于我们观察分析;
而使用动态链接时, 部分函数可能在运行时才由动态链接器进行链接, 不利于我们分析.

再次反汇编: objdump --disassemble --source <executable> > <output>

# ...
 
00000000004018b5 <main>:
#include <stdio.h>
 
int main() {
  4018b5: f3 0f 1e fa           endbr64
  4018b9: 55                    push   %rbp
  4018ba: 48 89 e5              mov    %rsp,%rbp
  puts("Hello World!");
  4018bd: 48 8d 05 4c d7 07 00  lea    0x7d74c(%rip),%rax        # 47f010 <__rseq_flags+0xc>
  4018c4: 48 89 c7              mov    %rax,%rdi
  4018c7: e8 64 34 00 00        call   404d30 <_IO_puts>
  return 0;
  4018cc: b8 00 00 00 00        mov    $0x0,%eax
}
  4018d1: 5d                    pop    %rbp
  4018d2: c3                    ret
  4018d3: 66 2e 0f 1f 84 00 00  cs nopw 0x0(%rax,%rax,1)
  4018da: 00 00 00 
  4018dd: 0f 1f 00              nopl   (%rax)
 
 
# ...
 
0000000000404d30 <_IO_puts>:
  404d30: f3 0f 1e fa           endbr64
  404d34: 55                    push   %rbp
  404d35: 48 89 e5              mov    %rsp,%rbp
  404d38: 41 56                 push   %r14
  404d3a: 41 55                 push   %r13
  404d3c: 41 54                 push   %r12
  404d3e: 49 89 fc              mov    %rdi,%r12
  404d41: 53                    push   %rbx
  404d42: 48 83 ec 10           sub    $0x10,%rsp
  404d46: e8 15 c4 ff ff        call   401160 <_init+0x160>
  404d4b: 4c 8b 2d 7e 59 0a 00  mov    0xa597e(%rip),%r13        # 4aa6d0 <stdout>
  404d52: 48 89 c3              mov    %rax,%rbx
  404d55: 41 f7 45 00 00 80 00  testl  $0x8000,0x0(%r13)
  404d5c: 00 
  404d5d: 0f 84 c5 00 00 00     je     404e28 <_IO_puts+0xf8>
  404d63: 4c 89 ef              mov    %r13,%rdi
  404d66: 8b 87 c0 00 00 00     mov    0xc0(%rdi),%eax
  404d6c: 85 c0                 test   %eax,%eax
  404d6e: 0f 85 0b 01 00 00     jne    404e7f <_IO_puts+0x14f>
  404d74: c7 87 c0 00 00 00 ff  movl   $0xffffffff,0xc0(%rdi)
  404d7b: ff ff ff 
  404d7e: 4c 8b b7 d8 00 00 00  mov    0xd8(%rdi),%r14
  404d85: 48 8d 15 74 2c 0a 00  lea    0xa2c74(%rip),%rdx        # 4a7a00 <__io_vtables>
  404d8c: 4c 89 f0              mov    %r14,%rax
  404d8f: 48 29 d0              sub    %rdx,%rax
  404d92: 48 3d 2f 09 00 00     cmp    $0x92f,%rax
  404d98: 0f 87 6a 01 00 00     ja     404f08 <_IO_puts+0x1d8>
  404d9e: 48 89 da              mov    %rbx,%rdx
  404da1: 4c 89 e6              mov    %r12,%rsi
  404da4: 41 ff 56 38           call   *0x38(%r14)
  404da8: 48 39 c3              cmp    %rax,%rbx
  404dab: 0f 85 d7 00 00 00     jne    404e88 <_IO_puts+0x158>
  404db1: 48 8b 3d 18 59 0a 00  mov    0xa5918(%rip),%rdi        # 4aa6d0 <stdout>
  404db8: 48 8b 47 28           mov    0x28(%rdi),%rax
  404dbc: 48 3b 47 30           cmp    0x30(%rdi),%rax
  404dc0: 0f 83 5a 01 00 00     jae    404f20 <_IO_puts+0x1f0>
  404dc6: 48 8d 50 01           lea    0x1(%rax),%rdx
  404dca: 48 89 57 28           mov    %rdx,0x28(%rdi)
  404dce: c6 00 0a              movb   $0xa,(%rax)
  404dd1: 48 83 c3 01           add    $0x1,%rbx
  404dd5: b8 ff ff ff 7f        mov    $0x7fffffff,%eax
  404dda: 48 39 c3              cmp    %rax,%rbx
  404ddd: 48 0f 46 c3           cmovbe %rbx,%rax
  404de1: 41 f7 45 00 00 80 00  testl  $0x8000,0x0(%r13)
  404de8: 00 
  404de9: 75 2d                 jne    404e18 <_IO_puts+0xe8>
  404deb: 49 8b bd 88 00 00 00  mov    0x88(%r13),%rdi
  404df2: 80 3d 5f 62 0a 00 00  cmpb   $0x0,0xa625f(%rip)        # 4ab058 <__libc_single_threaded>
  404df9: 8b 57 04              mov    0x4(%rdi),%edx
  404dfc: 0f 84 ae 00 00 00     je     404eb0 <_IO_puts+0x180>
  404e02: 85 d2                 test   %edx,%edx
  404e04: 0f 85 aa 00 00 00     jne    404eb4 <_IO_puts+0x184>
  404e0a: 48 c7 47 08 00 00 00  movq   $0x0,0x8(%rdi)
  404e11: 00 
  404e12: c7 07 00 00 00 00     movl   $0x0,(%rdi)
  404e18: 48 83 c4 10           add    $0x10,%rsp
  404e1c: 5b                    pop    %rbx
  404e1d: 41 5c                 pop    %r12
  404e1f: 41 5d                 pop    %r13
  404e21: 41 5e                 pop    %r14
  404e23: 5d                    pop    %rbp
  404e24: c3                    ret
  404e25: 0f 1f 00              nopl   (%rax)
  404e28: 49 8b bd 88 00 00 00  mov    0x88(%r13),%rdi
  404e2f: 80 3d 22 62 0a 00 00  cmpb   $0x0,0xa6222(%rip)        # 4ab058 <__libc_single_threaded>
  404e36: 64 4c 8b 34 25 10 00  mov    %fs:0x10,%r14
  404e3d: 00 00 
  404e3f: 48 8b 47 08           mov    0x8(%rdi),%rax
  404e43: 75 53                 jne    404e98 <_IO_puts+0x168>
  404e45: 49 39 c6              cmp    %rax,%r14
  404e48: 0f 84 aa 00 00 00     je     404ef8 <_IO_puts+0x1c8>
  404e4e: 31 c0                 xor    %eax,%eax
  404e50: ba 01 00 00 00        mov    $0x1,%edx
  404e55: f0 0f b1 17           lock cmpxchg %edx,(%rdi)
  404e59: 0f 85 e1 00 00 00     jne    404f40 <_IO_puts+0x210>
  404e5f: 49 8b 85 88 00 00 00  mov    0x88(%r13),%rax
  404e66: 48 8b 3d 63 58 0a 00  mov    0xa5863(%rip),%rdi        # 4aa6d0 <stdout>
  404e6d: 4c 89 70 08           mov    %r14,0x8(%rax)
  404e71: 8b 87 c0 00 00 00     mov    0xc0(%rdi),%eax
  404e77: 85 c0                 test   %eax,%eax
  404e79: 0f 84 f5 fe ff ff     je     404d74 <_IO_puts+0x44>
  404e7f: 83 f8 ff              cmp    $0xffffffff,%eax
  404e82: 0f 84 f6 fe ff ff     je     404d7e <_IO_puts+0x4e>
  404e88: b8 ff ff ff ff        mov    $0xffffffff,%eax
  404e8d: e9 4f ff ff ff        jmp    404de1 <_IO_puts+0xb1>
  404e92: 66 0f 1f 44 00 00     nopw   0x0(%rax,%rax,1)
  404e98: 48 85 c0              test   %rax,%rax
  404e9b: 75 a8                 jne    404e45 <_IO_puts+0x115>
  404e9d: c7 07 01 00 00 00     movl   $0x1,(%rdi)
  404ea3: 4c 89 77 08           mov    %r14,0x8(%rdi)
  404ea7: e9 b7 fe ff ff        jmp    404d63 <_IO_puts+0x33>
  404eac: 0f 1f 40 00           nopl   0x0(%rax)
  404eb0: 85 d2                 test   %edx,%edx
  404eb2: 74 1c                 je     404ed0 <_IO_puts+0x1a0>
  404eb4: 83 ea 01              sub    $0x1,%edx
  404eb7: 89 57 04              mov    %edx,0x4(%rdi)
  404eba: 48 83 c4 10           add    $0x10,%rsp
  404ebe: 5b                    pop    %rbx
  404ebf: 41 5c                 pop    %r12
  404ec1: 41 5d                 pop    %r13
  404ec3: 41 5e                 pop    %r14
  404ec5: 5d                    pop    %rbp
  404ec6: c3                    ret
  404ec7: 66 0f 1f 84 00 00 00  nopw   0x0(%rax,%rax,1)
  404ece: 00 00 
  404ed0: 48 c7 47 08 00 00 00  movq   $0x0,0x8(%rdi)
  404ed7: 00 
  404ed8: 87 17                 xchg   %edx,(%rdi)
  404eda: 83 fa 01              cmp    $0x1,%edx
  404edd: 0f 8e 35 ff ff ff     jle    404e18 <_IO_puts+0xe8>
  404ee3: 89 45 dc              mov    %eax,-0x24(%rbp)
  404ee6: e8 25 68 00 00        call   40b710 <__lll_lock_wake_private>
  404eeb: 8b 45 dc              mov    -0x24(%rbp),%eax
  404eee: e9 25 ff ff ff        jmp    404e18 <_IO_puts+0xe8>
  404ef3: 0f 1f 44 00 00        nopl   0x0(%rax,%rax,1)
  404ef8: 83 47 04 01           addl   $0x1,0x4(%rdi)
  404efc: e9 62 fe ff ff        jmp    404d63 <_IO_puts+0x33>
  404f01: 0f 1f 80 00 00 00 00  nopl   0x0(%rax)
  404f08: e8 a3 02 00 00        call   4051b0 <_IO_vtable_check>
  404f0d: 48 8b 3d bc 57 0a 00  mov    0xa57bc(%rip),%rdi        # 4aa6d0 <stdout>
  404f14: e9 85 fe ff ff        jmp    404d9e <_IO_puts+0x6e>
  404f19: 0f 1f 80 00 00 00 00  nopl   0x0(%rax)
  404f20: be 0a 00 00 00        mov    $0xa,%esi
  404f25: e8 d6 3a 00 00        call   408a00 <__overflow>
  404f2a: 83 f8 ff              cmp    $0xffffffff,%eax
  404f2d: 0f 85 9e fe ff ff     jne    404dd1 <_IO_puts+0xa1>
  404f33: e9 50 ff ff ff        jmp    404e88 <_IO_puts+0x158>
  404f38: 0f 1f 84 00 00 00 00  nopl   0x0(%rax,%rax,1)
  404f3f: 00 
  404f40: e8 0b 67 00 00        call   40b650 <__lll_lock_wait_private>
  404f45: e9 15 ff ff ff        jmp    404e5f <_IO_puts+0x12f>
  404f4a: f3 0f 1e fa           endbr64
  404f4e: 48 89 c3              mov    %rax,%rbx
  404f51: e9 2a c2 ff ff        jmp    401180 <_IO_puts.cold>
  404f56: 66 2e 0f 1f 84 00 00  cs nopw 0x0(%rax,%rax,1)
  404f5d: 00 00 00 
 
# ...

主函数里那一句 call 后面已经被填入了一个地址.
从反汇编代码中我们也可以看到, 这个地址就在这个可执行文件里, 其中包含我们所调用的函数的具体实现.
由此,外部函数的实现是在链接这一步骤中被插入到最终的可执行文件中的, 而不是直接以源代码形式和我们编写的其它源代码一起编译.

Compile and Link

对于含有多个.c 文件的工程来说, 编译器会首先将所有的 .c 文件以文件为单位, 编译成 .o 文件.
最后再将所有的.o 文件以及函数库链接在一起, 形成最终的可执行文件.

ELF

ELF for Executable and Linkable Format

目标文件和可执行文件都是用 ELF 格式记录的, 至于具体类型则在 ELF 文件头 里说明.

格式

ELF Structure

ELF 由一个头, 两个表组成.

  1. ELF 头, 包括程序的基本信息, 比如体系结构和操作系统,同时也包含了节头表和段头表相对文件的偏移量(offset).
  2. 段头表(或程序头表, program header table), 主要包含程序中各个段(segment)的信息, 段的信息需要在运行时刻使用.
  3. 节头表(section header table), 主要包含程序中各个节(section)的信息,节的信息需要在程序编译和链接的时候使用.
  4. 段头表中的每一个表项, 记录了该段数据载入内存时的目标位置等, 记录了用于指导应用程序加载的各类信息.
  5. 节头表中的每一个表项, 记录了该节程序的代码段(.text)或数据段(.data)等各个段的内容的类型, 主要是链接器在链接的过程中需要使用.

因此段和节是程序的两种不同看待数据的方式:

  1. 组成可重定位文件, 参与可执行文件和可共享文件的链接. 此时使用节头表.
  2. 组成可执行文件或者可共享文件, 在运行时为加载器提供信息. 此时使用段头表.

实际上, ELF 头就是一个如下的结构体:

#define EI_NIDENT (16)
 
typedef struct {
 unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
 Elf32_Half e_type;    /* Object file type */
 Elf32_Half e_machine;    /* Architecture */
 Elf32_Word e_version;    /* Object file version */
 Elf32_Addr e_entry;    /* Entry point virtual address */
 Elf32_Off e_phoff;    /* Program header table file offset */
 Elf32_Off e_shoff;    /* Section header table file offset */
 Elf32_Word e_flags;    /* Processor-specific flags */
 Elf32_Half e_ehsize;    /* ELF header size in bytes */
 Elf32_Half e_phentsize;    /* Program header table entry size */
 Elf32_Half e_phnum;    /* Program header table entry count */
 Elf32_Half e_shentsize;    /* Section header table entry size */
 Elf32_Half e_shnum;    /* Section header table entry count */
 Elf32_Half e_shstrndx;    /* Section header string table index */
} Elf32_Ehdr;
 
/* Fields in the e_ident array.  The EI_* macros are indices into the
   array.  The macros under each EI_* macro are the values the byte
   may have.  */
 
#define EI_MAG0 0    /* File identification byte 0 index */
#define ELFMAG0 0x7f /* Magic number byte 0 */
 
#define EI_MAG1 1   /* File identification byte 1 index */
#define ELFMAG1 'E' /* Magic number byte 1 */
 
#define EI_MAG2 2   /* File identification byte 2 index */
#define ELFMAG2 'L' /* Magic number byte 2 */
 
#define EI_MAG3 3   /* File identification byte 3 index */
#define ELFMAG3 'F' /* Magic number byte 3 */

而段和节头表是一系列结构体, 每个结构体如下:

/* Section segment header.  */
typedef struct {
 Elf32_Word sh_name;  /* Section name */
 Elf32_Word sh_type;  /* Section type */
 Elf32_Word sh_flags;  /* Section flags */
 Elf32_Addr sh_addr;  /* Section addr */
 Elf32_Off sh_offset;  /* Section offset */
 Elf32_Word sh_size;  /* Section size */
 Elf32_Word sh_link;  /* Section link */
 Elf32_Word sh_info;  /* Section extra info */
 Elf32_Word sh_addralign; /* Section alignment */
 Elf32_Word sh_entsize;  /* Section entry size */
} Elf32_Shdr;
 
/* Program segment header.  */
 
typedef struct {
 Elf32_Word p_type;   /* Segment type */
 Elf32_Off p_offset;  /* Segment file offset */
 Elf32_Addr p_vaddr;  /* Segment virtual address */
 Elf32_Addr p_paddr;  /* Segment physical address */
 Elf32_Word p_filesz; /* Segment size in file */
 Elf32_Word p_memsz;  /* Segment size in memory */
 Elf32_Word p_flags;  /* Segment flags */
 Elf32_Word p_align;  /* Segment alignment */
} Elf32_Phdr;

解析

我们可以用 readelf 工具来分析 ELF 文件内容, 常用参数有:

  • -h: 读取 ELF 头
  • -S: 读取节头表

Kernel 启动

启动并运行一个操作系统, 我们需要将其加载到内存的正确位置上.

那么, 何谓正确? 如何控制?

MIPS 内存布局

我们先看 MIPS 的内存布局, 从而确定我们要把内核加载到哪里.

MIPS Memory Layout

 o     4G ----------->  +----------------------------+------------0x100000000
 o                      |       ...                  |  kseg2
 o      KSEG2    -----> +----------------------------+------------0xc000 0000
 o                      |          Devices           |  kseg1
 o      KSEG1    -----> +----------------------------+------------0xa000 0000
 o                      |      Invalid Memory        |   /|\
 o                      +----------------------------+----|-------Physical Memory Max
 o                      |       ...                  |  kseg0
 o      KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------end
 o                      |       Kernel Stack         |    | KSTKSIZE            /|\
 o                      +----------------------------+----|------                |
 o                      |       Kernel Text          |    |                    PDMAP
 o      KERNBASE -----> +----------------------------+----|-------0x8002 0000    |
 o                      |      Exception Entry       |   \|/                    \|/
 o      ULIM     -----> +----------------------------+------------0x8000 0000-------
 o                      |         User VPT           |     PDMAP                /|\
 o      UVPT     -----> +----------------------------+------------0x7fc0 0000    |
 o                      |           pages            |     PDMAP                 |
 o      UPAGES   -----> +----------------------------+------------0x7f80 0000    |
 o                      |           envs             |     PDMAP                 |
 o  UTOP,UENVS   -----> +----------------------------+------------0x7f40 0000    |
 o  UXSTACKTOP -/       |     user exception stack   |     PTMAP                 |
 o                      +----------------------------+------------0x7f3f f000    |
 o                      |                            |     PTMAP                 |
 o      USTACKTOP ----> +----------------------------+------------0x7f3f e000    |
 o                      |     normal user stack      |     PTMAP                 |
 o                      +----------------------------+------------0x7f3f d000    |
 a                      |                            |                           |
 a                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~                           |
 a                      .                            .                           |
 a                      .                            .                         kuseg
 a                      .                            .                           |
 a                      |~~~~~~~~~~~~~~~~~~~~~~~~~~~~|                           |
 a                      |                            |                           |
 o       UTEXT   -----> +----------------------------+------------0x0040 0000    |
 o                      |      reserved for COW      |     PTMAP                 |
 o       UCOW    -----> +----------------------------+------------0x003f f000    |
 o                      |   reversed for temporary   |     PTMAP                 |
 o       UTEMP   -----> +----------------------------+------------0x003f e000    |
 o                      |       invalid memory       |                          \|/
 a     0 ------------>  +----------------------------+ ----------------------------
 o
  1. kuseg 0x00000000-0x7FFFFFFF(2 GB):
    这一段是用户态下唯一可用的地址空间(内核态下也可使用这段地址空间), 大小为 2 GB, 也就是 MIPS 约定的用户内存空间.
    需要通过 MMU(Memory Management Unit)中的 TLB 进行虚拟地址到物理地址的变换.
    对这段地址的存取都会通过 cache.
  2. kseg0 0x80000000-0x9FFFFFFF(512 MB):
    这一段是内核态下可用的地址, MMU 将地址的最高位清零(& 0x7fffffff)就得到物理地址用于访存.
    也就是说, 这段的虚拟地址被连续地映射到物理地址的低 512 MB 空间.
    对这段地址的存取都会通过 cache.
  3. kseg1 0xA0000000-0xBFFFFFFF(512 MB):
    与 kseg0 类似, 这段地址也是内核态下可用的地址, MMU 将虚拟地址的高三位清零 (& 0x1fffffff) 就得到物理地址用于访存.
    这段虚拟地址也被连续地映射到物理地址的低 512 MB 空间.
    但是对这段地址的存取不通过 cache, 往往在这段地址上使用 MMIO(Memory-Mapped I/O)技术来访问外设.
  4. kseg2 0xC0000000-0xFFFFFFFF(1 GB):
    这段地址只能在内核态下使用并且需要 MMU 中 TLB 将虚拟地址转换为物理地址.
    对这段地址的存取都会通过 cache.

我们将内核的 .text、.data、.bss 段都放到 kseg0 中.
因为 TLB 需要内核配置管理, 我们无法在没有载入内核的时候使用 TLB, 因此 kseg2 必然不行.
而 kseg1 不经过 cache, 一般只有访问外设的时候使用.

Note

PTMAP 是4KB 的区域, 对应一个 PTE 条目; PDMAP 是一个 4MB 的区域, 对应一个 PDE 条目. 详细请见 Memory Management.

Linker Script

接下来, 我们要看看如何控制内核被加载到这个位置.

编译器在生成 ELF 文件的时候就已经记录了各节需要被加载到的位置, 并且这个可执行文件实际上是由链接器产生的,
因此我们要控制它的行为, 让它链接的时候产生正确的地址, 这就是 Linker Script 所做的事.

Linker Script 中记录了各个节应该如何映射到段, 以及各个段应该被加载到的位置.

节的详解

在链接过程, 目标文件被视为节的集合, 并使用节头表, 描述各个节的组织. 其中最为重要的三个节为 .text、.data、.bss, 作用为:

  • .text 保存可执行文件的操作指令.
  • .data 保存已初始化的全局变量和静态变量。
  • .bss 保存未初始化的全局变量和静态变量。

以下一个例子可以看出来:

#include <stdio.h>
char msg[] = "Hello, World!";
int count;
int main() {
  printf("msg: %X\n", msg);
  printf("count: %X\n", &count);
  printf("main: %X\n", main);
  return 0;
}
/*
msg: CA29C004
count: CA29E024
main: CA29B139
*/
$ readelf -S link
There are 30 section headers, starting at offset 0x36d8:
 
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  ...
  [12] .text             PROGBITS         0000000000001040  00001040
       000000000000015e  0000000000000000  AX       0     0     16
  ...
  [24] .data             PROGBITS         0000000000004008  00003008
       0000000000000018  0000000000000000  WA       0     0     8
  [25] .bss              NOBITS           0000000000004020  00003020
       0000000000000008  0000000000000000  WA       0     0     4
  ...

这里面似乎对不太上, 其实是一些现代 gcc 设置导致的. 如果关闭的话, 会类似于:

80D4188
80D60A0
8048AAC
 
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 4] .text PROGBITS 08048140 000140 0620e4 00 AX 0 0 16
[22] .data PROGBITS 080d4180 08b180 000f20 00 WA 0 0 32
[23] .bss NOBITS 080d50c0 08c0a0 00136c 00 WA 0 0 64

就可以对上了.

Linker Script 的编写

SECTIONS
{
 . = 0x10000;
 .text : { *(.text) }
 . = 0x8000000;
 .data : { *(.data) }
 .bss : { *(.bss) }
}
  • .(Location Counter): 一个写字时的”光标”. 设置 . 等于多少, 接下来的内容就从哪里开始写, 在 SECTION 开始时为 0.
  • (.text): 这是一个通配符。意思是把所有输入的 .o 文件里的代码段, 全都塞进新定义的这个 .text 块里.

使用 gcc -o test test.c -T test.lds 来指定使用这个 linker script 进行编译.

段是由节组合而成的, 节的地址被调整了, 那么最终段的地址也会相应地被调整.

示例:

/*
 * Set the architecture to mips.
 */
OUTPUT_ARCH(mips)
 
/*
 * Set the ENTRY point of the program to _start.
 */
ENTRY(_start)
 
SECTIONS {
 . = 0x80020000;
 
 .text = { *(.text) }
 
 .data = { *(.data) }
 
 bss_start = .;
 .bss = { *(.bss) }
 
 bss_end = .;
 . = 0x80400000;
 end = . ;
}

其中, 三部分是连续的, 都在 Kernel Text(见上图)的位置.

特别地,

  1. .bss 前后标记开始结束, 是因为里面存的是未初始化的变量, 在磁盘不占用空间, 只记录大小
    内核开发者需要亲自动手写一段循环代码, 把这一块区域全部填成 0.
    为了让代码知道从哪开始填和填到哪结束, 就必须在 Linker Script 里用 bss_startbss_end 标记出边界.
  2. ENTRY(_start) 的作用
    在 ELF 文件的文件头里写下一个地址. 当你编译完内核生成 ELF 文件时, 链接器会查阅 Linker Script:
    链接器看到 ENTRY(_start), 它去所有的 .o 文件里找 _start 这个符号对应的内存地址(比如 0x80020000) 并把这个地址填入 ELF Header 的 e_entry 字段.