printk("Hello OS!");
Thinking 1.1 objdump 参数和其他体系结构编译链接过程#
gcc 编译过程#
gcc -E src.c > src.i
gcc -S src.i # result src.s
gcc -c src.s # result src.o
gcc src.o # result a.out
a.out 即 assemble out 的缩写。
如果我们编写一个最简单的 C 语言程序:
// src.c
void main() {}
// need void explicitly, default is int
经过前三个步骤,得到了 src.o,经过反汇编 objdump -DS --section=.text src.o 得到的代码如下:
src.o: 文件格式 elf32-tradbigmips  
  
Contents of section .text:  
0000 27bdfff8 afbe0004 03a0f025 00000000 '..........%....  
0010 03c0e825 8fbe0004 27bd0008 03e00008 ...%....'.......  
0020 00000000 00000000 00000000 00000000 ................  
  
Disassembly of section .text:  
  
00000000 <main>:  
0: 27bdfff8 addiu sp,sp,-8  
4: afbe0004 sw s8,4(sp)  
8: 03a0f025 move s8,sp  
c: 00000000 nop  
10: 03c0e825 move sp,s8  
14: 8fbe0004 lw s8,4(sp)  
18: 27bd0008 addiu sp,sp,8  
1c: 03e00008 jr ra  
20: 00000000 nop  
...
实践到这里我意识到,
...是对全零段的省略,并没有省略关键信息。
而直接使用 gcc 编译得到的可执行文件 a.out反汇编结果如下:
a.out: 文件格式 elf32-tradbigmips  
  
Contents of section .text:  
4004f0 03e00025 04110001 00000000 3c1c0002 ...%........<...  
400500 279c7b14 039fe021 0000f825 8f848018 '.{....!...%....  
400510 8fa50000 27a60004 2401fff8 03a1e824 ....'...$......$  
400520 27bdffe0 00003825 afa00010 afa20014 '.....8%........  
400530 afbd0018 8f998028 0320f809 00000000 .......(. ......  
400540 1000ffff 00000000 00000000 00000000 ................  
400550 3c040042 3c020042 24840014 24420014 <..B<..B$...$B..  
400560 10440007 3c1c0043 279c8010 8f998024 .D..<..C'......$  
400570 13200003 00000000 03200008 00000000 . ....... ......  
400580 03e00008 00000000 3c040042 3c020042 ........<..B<..B  
400590 24840014 24450014 00a42823 00051083 $...$E....(#....  
4005a0 00052fc2 00a22821 00052843 10a00007 ../...(!..(C....  
4005b0 3c1c0043 279c8010 8f99801c 13200003 <..C'........ ..  
4005c0 00000000 03200008 00000000 03e00008 ..... ..........  
4005d0 00000000 27bdffe0 afb00018 3c100042 ....'.......<..B  
4005e0 afbf001c 92020040 14400006 8fbf001c .......@.@......  
4005f0 0c100154 00000000 24020001 a2020040 ...T....$......@  
400600 8fbf001c 8fb00018 03e00008 27bd0020 ............'..  
400610 08100162 00000000 00000000 00000000 ...b............  
400620 27bdfff8 afbe0004 03a0f025 00000000 '..........%....  
400630 03c0e825 8fbe0004 27bd0008 03e00008 ...%....'.......  
400640 00000000 00000000 00000000 00000000 ................  
  
Disassembly of section .text:  
  
# 篇幅限制,省略后文
经过对比可以发现,一个默认的可执行文件除了包含编写的代码以外,还添加了:
- 程序入口点 
__start - 异常处理和程序终止 
hlt - 线程局部存储支持
 - 全局对象构造和析构的支持
 
此外,前一个目标文件的机器码地址从 0x0 开始,这是一种相对地址;后一个可执行文件的机器码地址从 0x4004f0 开始,这已经是程序执行时其虚拟地址空间中的绝对地址了。至于为什么不是从 0x0040_0000 开始,还有 1264 字节的偏移量,大概是在 __start 之前还有 elf 文件头等各种信息需要存储。
所以一个 C 语言程序运行的真正入口不是 main 函数,而是 __start。
当程序运行时,操作系统加载这个可执行文件,从 __start 入口点开始执行,初始化运行环境,然后调用 main 函数,最后在 main 返回后清理资源并退出。
objdump 参数解析#
objdump -s:默认显示所有非空节的完整内容。每行表现形式为起始地址+机器码+ASCII字符表示。objdump -S:尽可能反汇编出源代码。建议配合编译时-g参数使用。objdump -d:反汇编特定内容,比如main函数。objdump -D:反汇编所有的节。
Thinking 1.2 readelf 程序的问题#
执行结果#
0:0x0  
1:0x80020000  
2:0x80022090  
3:0x800220a8  
4:0x800220c0  
5:0x0  
6:0x0  
7:0x0  
8:0x0  
9:0x0  
10:0x0  
11:0x0  
12:0x0  
13:0x0  
14:0x0  
15:0x0  
16:0x0  
17:0x0  
18:0x0
上述执行结果代表内核 ELF 文件的各节头的地址信息。结合系统内置的 readelf程序的输出结果,可知上述结果准确无误。继续观察内置程序的输出,可以发现上述结果中唯一具有具体地址的第 1~4 节分别为 .text, .reginfo, .MIPS.abiflags, .rodata,而它们的 Flg 位都含有 A 标识(Allocate),表示在程序运行的过程中需要为这些节分配内存空间。
左脚踩右脚上天#
我们自己完善的 readelf 工具是无法解析自身的。其实,查看 elf.h 头文件便知,我们的程序仅支持 ELF 32 位的程序解析。事实上,我们的程序仅支持 ELF 32 位 小端顺序的程序,如果解析大端顺序的程序,那么会提示段错误。
真正的 readelf 程序依靠 ELF 头的信息,实现了架构无关性和字节序兼容性,使得这个工具可以解析几乎所有的 ELF 文件。
当我们运行 readelf -h $(which readelf) 来让 readelf 解析自身时,这个可执行程序被加载到内存中,同时它本身作为被解析的文件也被加载到内存中,二者的性质是不同的,因而可以实现自举。
我们自己完善的 readelf 工具是在 x64 环境下编译的,其自身是 64 位程序。所以,想要实现我们的 readelf 工具的自举,只需要把我们的程序编译成 32 位程序运行即可。由于在 Ubuntu 上 gcc-multilib 与 gcc-mips-linux-gnu 存在冲突,我们没办法直接使用 -m32 参数编译出 32 位程序,只能通过 mipsel-linux-gnu-gcc 来把程序编译为 mips32 环境下的 32 位小端顺序的 ELF 文件,然后使用 qemu 运行:
sudo apt install gcc-mipsel-linux-gnu qemu-user
mipsel-linux-gnu-gcc main.c readelf.c -o readelf -static
qemu-mipsel readelf readelf
如此便可以让我们自己的 readelf 工具解析自己了。
Thinking 1.3 内核入口地址的问题#
这个问题问的莫名其妙,很容易让不明所以的同学混淆硬件上电时复位地址和内核入口地址两个概念。还是来梳理一下内核的启动流程。
内核启动流程分析#
系统在上电之后,CPU 会跳转到复位地址 0xbfc0_0000。这是一个虚拟地址,位于 kseg1 中,因而对应的物理地址为(虚拟地址去掉高三位得到的)0x1fc0_0000。这个地址即硬件上已经烧写好的 BIOS 的启动地址,BIOS 将内核文件加载到内存中, 然后跳转到 Linker Script 中定义的 ENTRY(__start) 位置,这就是内核入口了。
所以内核入口地址和上电启动地址根本就是两个概念:前者被 Linker Script 确定,后者被硬件架构确定。上电启动地址是一切的开始,内核入口地址是硬件与软件的分割线。
上机#
exam#
考的是 vprintfmt 中 %k 自定义参数的实现,比较简单。
extra#
考的实现是 fmemprintf 和 fmemopen、fseed、fclose 四个函数的实现。
