Lab01

思考题

thinking1.1

参数

-D : Display assembler contents of all sections,即反汇编所有节的内容

-S : Intermix source code with disassembly,显示与反汇编结合的源代码

编译与反汇编Ⅰ

main.c

1
2
3
4
5
6
int main(){
int a = 1;
int b = 2;
int c = a + b;
return 0;
}

反汇编main.o(只截取main代码段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00000000 <main>:
0: 27bdffe0 addiu sp,sp,-32 //8 bytes
4: afbe0018 sw s8,24(sp) //store $s8 into stack
8: 03a0f021 move s8,sp //store $sp into s8
c: 24020001 li v0,1 //$v0 = 1
10: afc20010 sw v0,16(s8) //
14: 24020002 li v0,2
18: afc2000c sw v0,12(s8)
1c: 8fc30010 lw v1,16(s8)
20: 8fc2000c lw v0,12(s8)
24: 00621021 addu v0,v1,v0
28: afc20008 sw v0,8(s8)
2c: 00001021 move v0,zero
30: 03c0e821 move sp,s8
34: 8fbe0018 lw s8,24(sp)
38: 27bd0020 addiu sp,sp,32
3c: 03e00008 jr ra
40: 00000000 nop

反汇编main(只截取main代码段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
004000b0 <main>:
4000b0: 27bdffe0 addiu sp,sp,-32
4000b4: afbe0018 sw s8,24(sp)
4000b8: 03a0f021 move s8,sp
4000bc: 24020001 li v0,1
4000c0: afc20010 sw v0,16(s8)
4000c4: 24020002 li v0,2
4000c8: afc2000c sw v0,12(s8)
4000cc: 8fc30010 lw v1,16(s8)
4000d0: 8fc2000c lw v0,12(s8)
4000d4: 00621021 addu v0,v1,v0
4000d8: afc20008 sw v0,8(s8)
4000dc: 00001021 move v0,zero
4000e0: 03c0e821 move sp,s8
4000e4: 8fbe0018 lw s8,24(sp)
4000e8: 27bd0020 addiu sp,sp,32
4000ec: 03e00008 jr ra
4000f0: 00000000 nop

在链接时进行了重定位,main地址不再是0,而是0x4000b0。

编译与反汇编Ⅱ——反汇编vmlinux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
80010040 <main>:
80010040: 27bdffe8 addiu sp,sp,-24
80010044: afbf0010 sw ra,16(sp)
80010048: 3c048001 lui a0,0x8001
8001004c: 0c00428e jal 80010a38 <printf>
80010050: 24840af8 addiu a0,a0,2808
80010054: 3c048001 lui a0,0x8001
80010058: 24840b14 addiu a0,a0,2836
8001005c: 2405000a li a1,10
80010060: 3c068001 lui a2,0x8001
80010064: 0c00428e jal 80010a38 <printf>
80010068: 24c60b1c addiu a2,a2,2844
8001006c: 0c004024 jal 80010090 <mips_init>
80010070: 00000000 nop
80010074: 3c048001 lui a0,0x8001
80010078: 24840b24 addiu a0,a0,2852
8001007c: 24050014 li a1,20
80010080: 3c068001 lui a2,0x8001
80010084: 0c00429e jal 80010a78 <_panic>
80010088: 24c60b2c addiu a2,a2,2860
8001008c: 00000000 nop

thinking1.2

之前的testELF文件是小端编码,而内核文件vmlinux是大端编码,在读取数据时会产生错误,最终导致读取地址越界,因此我们的readELF程序并不能解析。

thinking1.3

因为在GXemul仿真器的帮助下,我们已经有了完整的C环境,只需要将内核加载后跳转到内核函数入口就可以启动完毕。

在链接器中,我们指定了内核加载的地址,并通过start中的代码,初始化硬件设备,设置堆栈入口,然后跳转到了内核函数入口处。

thinking1.4

避免页面冲突现象,即两个程序所占空间不能重合;

避免页面共享现象,即两个程序占据了同一页的空间。

对于当前程序加载时,应当以前一程序的尾地址向后页对齐后的地址作为起始地址,即保证两个程序不会占据同一页的空间,避免了页面共享和页面冲突现象。

thinking1.5

内核入口在0x80010000

main函数在0x80010040,从对vmlinux的反汇编中可以看到

start.S文件中设置堆栈入口后,通过调用main函数,即MIPS中的jal指令,进入main函数

通过链接和重定位后,每个函数有自己的地址,可以通过jal进行跨文件调用函数

thinking1.6

CP0的状态寄存器STATUS REGISTER置0,则其中的全局中断使能位也为0,禁用全局中断。

CP0CONFIG寄存器中值取出,将低三位置0又将第2位置1,则低三位K0区值为2,这时意为决定kseg0区不用高速缓存。

实验难点

难点一 ELF文件的解析

lab1-elf

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 */
// ELF文件头的大小(单位为字节)
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 {
// section name
Elf32_Word sh_name;
// section type
Elf32_Word sh_type;
// section flags
Elf32_Word sh_flags;
// section addr
Elf32_Addr sh_addr;
// offset from elf head of this entry
Elf32_Off sh_offset;
// byte size of this section
Elf32_Word sh_size;
// link
Elf32_Word sh_link;
// extra info
Elf32_Word sh_info;
// alignment
Elf32_Word sh_addralign;
// entry size
Elf32_Word sh_entsize;
}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;

这是Lab1的第一道难关,也是第一次上机的重要考点,在反复阅读代码以及ELF手册后,终于明白了ELF文件大概布局和格式。

ELF Header

每个ELF文件有且仅有一个ELF Header,也就是上述定义为Elf32_Ehdr的结构体,在这里记录了整个ELF文件的重要信息,其中最值得关注的,例如:

  • e_ident:是ELF文件的标识符,一共16个字节的内容,其中前4字节又被称为魔数,用于标识这是一个ELF文件;第6个字节是数据编码格式,用于判断是小端存储还是大端存储。
  • e_entry:程序入口的虚拟地址。
  • e_ehsize:ELF文件头的大小。
  • e_phoff/e_shoff:程序(节)头表的偏移地址,都是相对于ELF文件起始地址的偏移量
  • e_phentsize/e_shentsize:程序(节)头表中每一表项的大小。
  • e_phnum/e_shnum:程序(节)头表中表项的数量。

了解后三个变量的定义后,就基本可以做出exercise1.2了。

段与节

这是ELF文件中间的部分,也是文件真正的内容所在。

ELF文件有三种格式:可重定位文件,共享目标文件和可执行文件。

当文件为可重定位文件时,我们将中间部分称为节,这时在文件末尾的节头表相对更重要,它记录了所有节的信息;

当文件为另外两种类型时,我们将中间部分称为段,这时在文件头部的程序头表更为重要,它记录了所有段的信息。

总而言之,节与段只是在不同文件中表现形式的区别,实际上是同一部分内容。一般来说,段的数量会少于节,因为一个段一般会包含多个节的内容。

程序头表与节头表

从ELF文件布局图中看,这两个表一个在文件开头,一个在文件结尾(实际布局可能有所差异)。

之前提到过ELF文件头对应于一个ELF文件,在文件中有且仅有一个。

但是对于Elf32_Shdr, Elf32_Shdr两个结构体,每一个结构体就是表中的一个表项,对应于实际ELF文件中的一个节或段,记录了对应节或段的重要信息,其数量往往对应于文件中节或段的数量。

访问这两个表就需要我们借助ELF文件头中的信息。

exercise1.2中访问节头表为例,先用e_shoff加上文件的起始地址,访问节头表的基地址;然后以e_shentsize作偏移,依次访问每一个节头表项,而节头表项的数量从e_shnum即可得知。

难点二 实战printf

处理长参数表

这个其实并不难,按照指导书提供的参数表格式依次处理就好。

需要注意的是对于参数的初始化,如width = prec = 0, padc = ' '等。

补充打印整数部分

这部分也不难,相对其他部分,引入了negFlag这一变量,只需先对打印的参数进行判断,若为负数则将其变为正数并置negFlag为1。

阅读代码并分析函数功能

这一部分应该是完成exercise1.5的前置功课,这才是最难的部分,真正实操的部分反而简单。

一些宏函数和变量类型:

  • va_list
  • va_start
  • va_arg
  • va_end

以及三个local help functions

  • PrintNum
  • PrintChar
  • PrintString

实验心得与总结

总的来说这一次实验总体难度水平中等偏难,相对于Lab0的入门实验,难度肯定是有了较大的提升。但是作为后续实验的基础来说,后续的实验只会更难。

在刚开始阅读指导书的时候,什么都不知道,一头雾水。能把每个分散的exercise做好,但是对整体的知识体系结构还是一无所知。

在完成printf实战之后再回头梳理整个脉络后,就会对Lab1的整体结构有个较为清楚的把握。

以总的Makefile为基础,可以将Lab1的几乎所有文件夹串联起来:

  • boot文件夹中的start.S主要与启动相关,负责初始化硬件,设置堆栈入口,以及跳转到main函数

  • drivers文件夹主要与外部硬件设备相关,如实现输出字符的地址定义就在这里console.c文件中

  • include文件夹主要包括了一些库函数,宏定义文件

  • lib文件夹则是包含了实现printf的文件

  • tools文件夹中是负责链接的文件,定义了内核入口地址

  • init文件夹中则是初始化和main函数文件,在Lab1中体现不多,在Lab2则是重点

  • readelf则是为了让我们手写函数去解析ELF文件

  • gxemul中是仿真器的位置

理解以后自然觉得不难,这样梳理以后就会对整体结构清晰很多。