实验思考题
Thinking 1.1
使用命令
/OSLAB/compiler/usr/bin/mips_4KC-gcc -c test1.c
/OSLAB/compiler/usr/bin/mips_4KC-ld test1.o -o test1
/OSLAB/compiler/usr/bin/mips_4KC-objdump -DS test1.o > o.txt
/OSLAB/compiler/usr/bin/mips_4KC-objdump -DS test1 > out.txt
反编译.o文件:
2 test1.o: file format elf32-tradbigmips
3
4 Disassembly of section .text:
5
6 00000000 <main>:
7 0: 3c1c0000 lui gp,0x0
8 4: 279c0000 addiu gp,gp,0
9 8: 0399e021 addu gp,gp,t9
10 c: 27bdffe0 addiu sp,sp,-32
11 10: afbe0018 sw s8,24(sp)
12 14: 03a0f021 move s8,sp
13 18: 24020003 li v0,3
14 1c: afc20010 sw v0,16(s8)
15 20: c7c00010 lwc1 $f0,16(s8)
16 24: 468000a1 cvt.d.w $f2,$f0
17 28: 8f820000 lw v0,0(gp)
18 2c: d4400000 ldc1 $f0,0(v0)
19 30: 46201082 mul.d $f2,$f2,$f0
20 34: c7c00010 lwc1 $f0,16(s8)
21 38: 46800021 cvt.d.w $f0,$f0
22 3c: 46201002 mul.d $f0,$f2,$f0
23 40: f7c00008 sdc1 $f0,8(s8)
24 44: 00001021 move v0,zero
25 48: 03c0e821 move sp,s8
26 4c: 8fbe0018 lw s8,24(sp)
27 50: 27bd0020 addiu sp,sp,32
28 54: 03e00008 jr ra
29 58: 00000000 nop
30 5c: 00000000 nop
31 Disassembly of section .reginfo:
32
33 00000000 <.reginfo>:
34 0: f2000004 0xf2000004
35 4: 00000000 nop
36 8: 00000005 0x5
37 ...
38 Disassembly of section .pdr:
39
40 00000000 <.pdr>:
41 0: 00000000 nop
42 4: 40000000 mfc0 zero,c0_index
43 8: fffffff8 sdc3 $31,-8(ra)
44 ...
45 14: 00000020 add zero,zero,zero
46 18: 0000001e 0x1e
47 1c: 0000001f 0x1f
48 Disassembly of section .rodata:
49
50 00000000 <.rodata>:
51 0: 40091eb8 0x40091eb8
52 4: 51eb851f beql t7,t3,fffe1484 <main+0xfffe1484>
53 8: 00000000 nop
54 c: 00000000 nop
55 ...
反编译.out文件:
2 test1: file format elf32-tradbigmips
3
4 Disassembly of section .reginfo:
5
6 00400094 <.reginfo>:
7 400094: f2000004 0xf2000004
8 400098: 00000000 nop
9 40009c: 00000005 0x5
10 ...
11 4000a8: 10007ff0 b 42006c <main+0x1ffbc>
12 Disassembly of section .text:
13
14 004000b0 <main>:
15 4000b0: 3c1c0fc0 lui gp,0xfc0
16 4000b4: 279c7f40 addiu gp,gp,32576
17 4000b8: 0399e021 addu gp,gp,t9
18 4000bc: 27bdffe0 addiu sp,sp,-32
19 4000c0: afbe0018 sw s8,24(sp)
20 4000c4: 03a0f021 move s8,sp
21 4000c8: 24020003 li v0,3
22 4000cc: afc20010 sw v0,16(s8)
23 4000d0: c7c00010 lwc1 $f0,16(s8)
24 4000d4: 468000a1 cvt.d.w $f2,$f0
25 4000d8: 8f828018 lw v0,-32744(gp)
26 4000dc: d4400110 ldc1 $f0,272(v0)
27 4000e0: 46201082 mul.d $f2,$f2,$f0
28 4000e4: c7c00010 lwc1 $f0,16(s8)
29 4000e8: 46800021 cvt.d.w $f0,$f0
30 4000ec: 46201002 mul.d $f0,$f2,$f0
31 4000f0: f7c00008 sdc1 $f0,8(s8)
32 4000f4: 00001021 move v0,zero
33 4000f8: 03c0e821 move sp,s8
34 4000fc: 8fbe0018 lw s8,24(sp)
35 400100: 27bd0020 addiu sp,sp,32
36 400104: 03e00008 jr ra
37 400108: 00000000 nop
38 40010c: 00000000 nop
39 Disassembly of section .rodata:
40
41 00400110 <.rodata>:
42 400110: 40091eb8 0x40091eb8
43 400114: 51eb851f beql t7,t3,3e1594 <main-0x1eb1c>
44 400118: 00000000 nop
45 40011c: 00000000 nop
46 Disassembly of section .got:
47
48 10000000 <_GLOBAL_OFFSET_TABLE_>:
49 10000000: 00000000 nop
50 10000004: 80000000 lb zero,0(zero)
51 10000008: 00400000 0x400000
52 ...
Thinking 1.2
使用readelf -h testELF解析testELF文件
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048490
Start of program headers: 52 (bytes into file)
Start of section headers: 4440 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 30
Section header string table index: 27
使用readelf -h vmlinux解析vmlinux文件
ELF Header:
Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, big endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: MIPS R3000
Version: 0x1
Entry point address: 0x80010000
Start of program headers: 52 (bytes into file)
Start of section headers: 37164 (bytes into file)
Flags: 0x1001, noreorder, o32, mips1
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 14
Section header string table index: 11
在Data段的信息中可以看到,我们生成的readelf文件适用于解析小端存储的,而我们的内核vmlinux是大端存储的文件,因此无法解析内核。
Thinking 1.3
实验操作系统使用GXemul仿真器,支持直接加载ELF格式的内核。BootLoader中Stage1的功能已经由其提供,而事实上此时已经跳到了内核入口,跳到内核第一步就是实现在boot/start.o中的代码初始化硬件设备,设置堆栈,跳转到main函数入口。
Thinking 1.4
在加载程序时,避免发生共享页面和冲突页面现象。首先,不同程序段的占用空间不能够有重合,然后,尽量避免一个页面同时被多个程序段所占用。即若前面的程序段末地址所占用的页面地址为vi,则后续的程序段首地址应从下一页面vi+1开始占用。
Thinking 1.5
内核入口在0x00000000处,main函数在0x80010000处。我们在start.S文件中编写mips汇编代码,设置堆栈后直接跳到main函数中。在跨文件调用函数时,每个函数会有一个固定的地址,调用过程为将需要存储的值进行进栈等保护,再用jal跳转到相应函数的地址。
Thinking 1.6
将宏定义的内容转化为寄存器编号:
mtc0 $0,$12
#将SR寄存器清零
mfc0 $t0,$16
and $t0,~0x7 #要清零的位的反码
ori $t0,0x2 #要置1的位
mtc0 $t0,$16
#将Config寄存器0号位和2号位置0,将1号位置1
上电后,需要设置SR寄存器来使CPU进入工作状态,而硬件通常都是复位后让许多寄存器的位为未定义。
Config寄存器的后三位为可写的,用来决定固定的kseg0区是否经过高速缓存和其确切的行为如何。
实验难点展示
本次实验要做的很简单很基础,当然了,这是基于我做完了之后得出来的结论。在那么多文件夹内找到彼此间的调用关系实属不易。我认为实验中较难的地方在于对ELF文件的解析和补充printf函数的部分。
Exercise 1.2 解析ELF文件
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;
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;
typedef struct {
// segment type
Elf32_Word p_type;
// offset from elf file head of this entry
Elf32_Off p_offset;
// virtual addr of this segment
Elf32_Addr p_vaddr;
// physical addr, in linux, this value is meanless and has same value of p_vaddr
Elf32_Addr p_paddr;
// file size of this segment
Elf32_Word p_filesz;
// memory size of this segment
Elf32_Word p_memsz;
// segment flag
Elf32_Word p_flags;
// alignment
Elf32_Word p_align;
}Elf32_Phdr;
ELF文件结构图如上图所示,而这三个结构体分别代表了ELF程序头、Section节头、Segment段头,实际上这个问题困扰了我很久。可以看到 ,这些结构体占用的空间都不大,所以这里面肯定不包含具体的内容。经过多次试验,我也终于想明白了
Elf32_Ehdr对应图标的ELF header,其中e_entry指明了程序入口的虚拟地址。e_phoff指明程序头表的偏移量。e_shoff 指明节头表的偏移量。e_phentsize 表示程序头表每一个表项的大小(即对应的Elf32_Phdr结构体大小)。e_phnum 表示程序头表项数目。e_shentsize 表示节头表每一个表项的大小(即对应的Elf32_Shdr结构体大小)。e_shnum 表示节头表项数目。
多个节头表项,可看做Elf32_Shdr[e_shnum]数组组成了节头表(Section Header Table)。通过对每个结构体的sh_addr属性的访问,可以轻松知道每个Section的具体位置。
多个程序头表项,可看做Elf32_Phdr[e_phnum]数组组成了程序头表(Programmer Header Table)。FileSiz代表该段的数据在文件中的长度。MemSiz代表该段的数据在内存中所应当占的大小,MemSiz永远大于等于FileSiz,C语言中未初始化的全局变量可以导致这个现象。
于是理清结构后,这个问题就十分简单了。程序入口地址是binary,节头起点和程序头起点分别为binary+e_shoff和binary+e_phoff。要想得到所有Section的地址,只需要声明一个Elf32_Shdr*类型的指针变量shdr,地址为shdr -> sh_addr,下一个节头的访问只需要shdr++(指针增加一,地址增加一个节头的大小)。当然了,知道每个节头的大小,也可以每次都加e_shentsize的偏移量来表示下一个节头的地址。
Exercise 1.5 补全print.c函数
两个难点,第一个是针对变长参数列表的分析,第二个是对local help functions
功能的分析。
<stdarg.h> 中定义了几个重要的宏:
typedef char* va_list;
//在调用参数表之前,定义一个va_list 类型的变量
void va_start ( va_list ap, prev_param );
//然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数。第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
type va_arg ( va_list ap, type );
//然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
void va_end ( va_list ap );
//获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end
在print.c函数中,*fmt指向格式化字符串现在分析到的一个字符,而va_arg则一直往后获取新的一个参数。对*fmt的分析是直接读字符串,碰到‘%’就进入下一步操作,碰到‘\0'就退出循环。获取格式的操作十分容易。而坑点主要在第二处代码补写的地方,由于%d是有符号十进制数,和其他的不一样,而在函数声明
int PrintNum(char * buf, unsigned long u, int base, int negFlag,
int length, int ladjust, char padc, int upcase)
可以注意到第二个参数类型是unsigned long
,这表明如果va_arg获取到了一个负数的话,则需要将其转化为正数,并将negFlag置1。
体会与感想
启动过程理解
在学习lab1的过程中,初步看了下整体的文件夹和代码。这些文件夹的名字取的都十分的恰当。以下是我的薄弱理解:boot用于在vmlinux启动时做出第一手操作;drivers实际上是和硬件强关联的,因为printf函数到了底层就靠他往某个地址写入字符;gxemul是我们的用于仿真的软件平台,make产生的vmlinux就在这里面;include是一些库函数吧,有着一部分的c函数库,还有对寄存器别名的宏定义等;init是内核启动后经由boot操作结束后的进一步初始化;lib中我们实现了printf函数,可以理解成一个我们自己实现的一个库文件夹;readelf主要是提供一个工具供我们解析ELF文件,貌似并不是启动所需的必须成分;tools提供一个链接器,让代码段和数据段存在于应该在的位置上。
由于GXemul仿真器的作用,我们相当于直接走到启动了内核这一步。在控制权转向内核后,start.o文件开始初始化硬件,设置堆栈,并将当前PC跳转到main函数入口。main函数的地址是在构建vmlinux的过程中,用tools/scse0_3.lds链接器链接到0x80010000地址上的,在init/main函数中,调用了mips_init函数,而printf函数所使用的库就在我们写的lib里面,这样在启动vmlinux时,显示屏就会打出三行语句。这也是为什么如果没实现1.5的内容是无法看到有效输出的,或许也是因为如此才无法通过1.3和1.4的评测吧。
总结