LAB1实验报告
thinking1.1
首先编写如下测试c程序:
1
2
3
4
5
6
7
|
#include <stdio.h>
int main(){
int a = 1;
printf("%d\n",a);
return 0;
}
|
使用gcc -c参数编译,使用objdump -DS 命令导出结果,取main函数部分的call命令观察如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
13: 8b 45 fc mov -0x4(%rbp),%eax
16: 89 c6 mov %eax,%esi
18: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 1f <main+0x1f>
1f: 48 89 c7 mov %rax,%rdi
22: b8 00 00 00 00 mov $0x0,%eax
27: e8 00 00 00 00 call 2c <main+0x2c>
2c: b8 00 00 00 00 mov $0x0,%eax
31: c9 leave
32: c3
|
可以看到call命令对应指令为e800000000,未链接
使用gcc ./test.o -o test链接文件,使用objdump -DS导出结果,main函数部分如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
0000000000001149 <main>:
1149: f3 0f 1e fa endbr64
114d: 55 push %rbp
114e: 48 89 e5 mov %rsp,%rbp
1151: 48 83 ec 10 sub $0x10,%rsp
1155: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
115c: 8b 45 fc mov -0x4(%rbp),%eax
115f: 89 c6 mov %eax,%esi
1161: 48 8d 05 9c 0e 00 00 lea 0xe9c(%rip),%rax # 2004 <_IO_stdin_used+0x4>
1168: 48 89 c7 mov %rax,%rdi
116b: b8 00 00 00 00 mov $0x0,%eax
1170: e8 db fe ff ff call 1050 <printf@plt>
1175: b8 00 00 00 00 mov $0x0,%eax
117a: c9 leave
117b: c3 ret
|
可见指令为e8 db fe ff ff
,说明文件已经链接完成,同时该程序内部有大量相关代码。
使用带有mips-linux-gnu-前缀的命令重复上述实验:
链接前的.o文件:
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
|
00000000 <main>:
0: 27bdffd8 addiu sp,sp,-40
4: afbf0024 sw ra,36(sp)
8: afbe0020 sw s8,32(sp)
c: 03a0f025 move s8,sp
10: 3c1c0000 lui gp,0x0
14: 279c0000 addiu gp,gp,0
18: afbc0010 sw gp,16(sp)
1c: 24020001 li v0,1
20: afc2001c sw v0,28(s8)
24: 8fc5001c lw a1,28(s8)
28: 3c020000 lui v0,0x0
2c: 24440000 addiu a0,v0,0
30: 8f820000 lw v0,0(gp)
34: 0040c825 move t9,v0
38: 0320f809 jalr t9
3c: 00000000 nop
40: 8fdc0010 lw gp,16(s8)
44: 00001025 move v0,zero
48: 03c0e825 move sp,s8
4c: 8fbf0024 lw ra,36(sp)
50: 8fbe0020 lw s8,32(sp)
54: 27bd0028 addiu sp,sp,40
58: 03e00008 jr ra
5c: 00000000 nop
|
推测其中的38位置为调用对应的指令。
链接后的文件中main对应位置:
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
|
004006e0 <main>:
4006e0: 27bdffd8 addiu sp,sp,-40
4006e4: afbf0024 sw ra,36(sp)
4006e8: afbe0020 sw s8,32(sp)
4006ec: 03a0f025 move s8,sp
4006f0: 3c1c0042 lui gp,0x42
4006f4: 279c9010 addiu gp,gp,-28656
4006f8: afbc0010 sw gp,16(sp)
4006fc: 24020001 li v0,1
400700: afc2001c sw v0,28(s8)
400704: 8fc5001c lw a1,28(s8)
400708: 3c020040 lui v0,0x40
40070c: 24440830 addiu a0,v0,2096
400710: 8f828030 lw v0,-32720(gp)
400714: 0040c825 move t9,v0
400718: 0320f809 jalr t9
40071c: 00000000 nop
400720: 8fdc0010 lw gp,16(s8)
400724: 00001025 move v0,zero
400728: 03c0e825 move sp,s8
40072c: 8fbf0024 lw ra,36(sp)
400730: 8fbe0020 lw s8,32(sp)
400734: 27bd0028 addiu sp,sp,40
400738: 03e00008 jr ra
40073c: 00000000 nop
|
可见此时main函数已经连接完成,jalr的目标来自-32720(gp),而gp在之前的指令经过了一系列处理。之所以链接过程与x86不同可能是由于指令集的差异导致。
其中objdump的参数-DS中-D表示反汇编所有section,-S表示隐含-d参数的同时尽可能反汇编出源代码。然而在此次测试中由于gcc没有指定-g参数,难以反汇编出源代码。
thinking1.2
使用编写的readelf显示如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
0:0x0
1:0x80010000
2:0x80011d80
3:0x80011d98
4:0x80011db0
5:0x0
6:0x0
7:0x0
8:0x0
9:0x0
10:0x0
11:0x0
12:0x0
13:0x0
14:0x0
15:0x0
16:0x0
|
对比系统自带的readelf,可见解析正确。
之所以无法解析自身,是由于gcc的编译参数导致。Makefile文件中的相关参数如下:
1
2
3
4
5
|
readelf: main.o readelf.o
$(CC) $^ -o $@
hello: hello.c
$(CC) $^ -o $@ -m32 -static -g
|
其中编译readelf时所用的参数没有-m32
而编译hello所用的参数中有。而作业中编写的readelf中使用的是Elf32_Ehdr参数头,这意味着其只能适用于32位程序。因此,默认使用64位编译的readelf无法解析自身。
使用readelf验证如下:
1
2
3
4
5
|
git@20375061:~/20375061/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)
|
1
2
3
4
5
|
git@20375061:~/20375061/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)
|
thinking1.3
启动过程分别由bootloader的两次boot和移交到内核代码后的启动过程完成。bootloader程序需要在BFC00000处既1FC00000物理地址处完成相关的启动操作,通过bootloader完成stage1并复制stage2代码到RAM并跳转,stage2将内核镜像读取到RAM中,最后跳转内核入口函数地址。如上过程将不同硬件的启动地址通过bootloader屏蔽,使得编写的内核不需要考虑最初的启动地址。在实验环境中,我们没有负责初始启动相关的代码编写而是由模拟器完成。
难点分析
在本次作业中,难点之一在于理解ELF这一目标文件格式。它分为可重定位文件(-c编译出的.0文件),可执行文件和共享对象问及那。首先要理解的是gcc编译出的未链接文件和链接后的问及那都属于ELF文件,并且可以用同一种方法解析。
ELF分为五个部分,ELF头包括了基本信息,并给出了节头表和程序头表(段头表)的位置。ELF头的各项是理解的关键,其中e_phoff记录了程序头表相对ELF头的位置,e_shoff记录了节头表相对ELF头的位置,e_[sh/ph][entsize/num]记录了节头/程序头表项的大小/数量。我们只需要根据这些属性既可以定位所有的程序头和端头表象位置。值得注意一个段中可能包含多个,而段完全可能不4k对齐,相邻段也可能发生冲突出现共用页面。
其中节头表项中sh_addr代表该节要加载到的虚拟地址位置,sh_size代表该节大小,而sh_addr代表该节的数据文件在elf文件中的位置。
难点之二在于对于裸机上的printk的编写的理解。对于库函数,我们必须要从最基础的内存位置出发编写显示代码,并且考虑到各种边界条件,对于c语言的指针的应用也有较高的要求。
实验体会
在本次实验中,较为深入地了解了ELF文件的格式,并且编写了针对32为ELF文件的SH解析代码。在编写printk的过程中,我们深入了解了各种格式化标签的解析过程,并完成了解析代码的编写。这使得我们在应用printf等库函数的过程中会对于其背后的实现原理由更深入的理解。