思考题
Thinking 1.1
objdump
的常用参数有如下一些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| -C --demangle 将底层的符号名解码成用户级名字,除了去掉所开头的下划线之外,还使得C++函数名以可理解的方式显示出来。
--debugging -g 显示调试信息。企图解析保存在文件中的调试信息并以C语言的语法显示出来。仅仅支持某些类型的调试信息。有些其他的格式被readelf -w支持。
-e --debugging-tags 类似-g选项,但是生成的信息是和ctags工具相兼容的格式。
--disassemble -d 从objfile中反汇编那些特定指令机器码的section。
-D --disassemble-all 与 -d 类似,但反汇编所有section.
-f --file-headers 显示objfile中每个文件的整体头部摘要信息。
-h --section-headers --headers 显示目标文件各个section的头部摘要信息。
-i --info 显示对于 -b 或者 -m 选项可用的架构和目标格式列表。
-j name --section=name 仅仅显示指定名称为name的section的信息
-l --line-numbers 用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用使用-ld和使用-d的区别不是很大,在源码级调试的时候有用,要求编译时使用了-g之类的调试编译选项。
-m machine --architecture=machine 指定反汇编目标文件时使用的架构,当待反汇编文件本身没描述架构信息的时候(比如S-records),这个选项很有用。可以用-i选项列出这里能够指定的架构.
-s --full-contents 显示指定section的完整内容。默认所有的非空section都会被显示。
-S --source 尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。
|
因此objdump -DS
的含义是反汇编所有section
的内容
我们先新建一个简单的t.c
文件,其内容为
1 2 3 4 5 6
| #include <stdio.h>
int main() { printf("hello world"); return 0; }
|
首先使用x86
工具链对其进行编译与反汇编,得到如下代码
1 2 3 4 5 6 7 8 9 10 11
| 0000000000001149 <main>: 1149: f3 0f 1e fa endbr64 114d: 55 push %rbp 114e: 48 89 e5 mov %rsp,%rbp 1151: 48 8d 05 ac 0e 00 00 lea 0xeac(%rip),%rax # 2004 <_IO_stdin_used+0x4> 1158: 48 89 c7 mov %rax,%rdi 115b: b8 00 00 00 00 mov $0x0,%eax 1160: e8 eb fe ff ff call 1050 <printf@plt> 1165: b8 00 00 00 00 mov $0x0,%eax 116a: 5d pop %rbp 116b: c3 ret
|
再使用mips-linux-gnu-
交叉编译工具编译与反汇编,得到如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 00000000 <main>: 0: 27bdffe0 addiu sp,sp,-32 4: afbf001c sw ra,28(sp) 8: afbe0018 sw s8,24(sp) c: 03a0f025 move s8,sp 10: 3c1c0000 lui gp,0x0 14: 279c0000 addiu gp,gp,0 18: afbc0010 sw gp,16(sp) 1c: 3c020000 lui v0,0x0 20: 24440000 addiu a0,v0,0 24: 8f820000 lw v0,0(gp) 28: 0040c825 move t9,v0 2c: 0320f809 jalr t9 30: 00000000 nop 34: 8fdc0010 lw gp,16(s8) 38: 00001025 move v0,zero 3c: 03c0e825 move sp,s8 40: 8fbf001c lw ra,28(sp) 44: 8fbe0018 lw s8,24(sp) 48: 27bd0020 addiu sp,sp,32 4c: 03e00008 jr ra 50: 00000000 nop ...
|
Thinking 1.2
使用自己编写的readelf
程序解析mos
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| git@23373193:~/23373193/tools/readelf (lab1)$ ./readelf ~/23373193/target/mos 0:0x0 1:0x80020000 2:0x800220e0 3:0x800220f8 4:0x80022110 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
|
使用readelf
工具解析分别解析我们自己编写的readelf
以及hello
文件,得到如下结果
1 2 3 4 5 6 7 8 9 10 11 12 13
| git@23373193:~/23373193/tools/readelf (lab1)$ readelf -h readelf ELF 头: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 类别: ELF64 数据: 2 补码,小端序 (little endian) ...
git@23373193:~/23373193/tools/readelf (lab1)$ readelf -h hello ELF 头: Magic: 7f 45 4c 46 01 01 01 03 00 00 00 00 00 00 00 00 类别: ELF32 数据: 2 补码,小端序 (little endian) ...
|
可以发现,hello
文件是32位的,我们的readelf
是64位的,而我们编写的readelf
仅能解析32位的ELF文件
Thinking 1.3
启动第一阶段由bootloader运行,初始化硬件设备,第二阶段运行操作系统内核加载器,执行引导程序。在实验中,我们使用Linker Script控制加载地址,让各个section被加载到指定地址,同时,kernel.lds
中指明了程序入口,保证能够正确跳转到操作系统内核
实验难点
ELF文件解析
通过阅读指导书我们发现,节头表和段头表指向相同的位置,因此我们选择更方便遍历的段头表开始进行遍历。通过阅读elf.h
,我们发现可以ELF的文件头获得段头表的相对偏移、段头表项数量等信息。之后,我们将节头表指针指向节头表所在位置,根据以上信息开始遍历即可。
vprintfmt

vprintfmt
这个函数的整体逻辑就是这样的
心得体会
本次实验带给我最大的收获应该是教会了我怎么在qemu中使用gdb调试代码。虽然vprintfmt
的主题逻辑并不复杂,但是依然有一些小的点需要通过一步一步调试并且跟踪代码的运行过程才能彻底理解。