BUAA-OS-2023-Lab1-Report

Thinking 1.1

不使用交叉编译,使用gcc -c对文件进行编译,对编译而尚未链接的文件进行反汇编可以得到以下代码:

git@21371068:~/21371068/tools/readelf (lab1)$ gcc -c hello.c 
git@21371068:~/21371068/tools/readelf (lab1)$ objdump -DS hello.o

hello.o: 文件格式 elf64-x86-64


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 8d 05 00 00 00 00 lea 0x0(%rip),%rax # f <main+0xf>
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: 5d pop %rbp
1d: c3 ret
...

不使用交叉编译,对编译出的可执行文件直接进行objdump -DS指令,可以得到以下代码:

git@21371068:~/21371068/tools/readelf (lab1)$ gcc hello.c -o hello2
git@21371068:~/21371068/tools/readelf (lab1)$ objdump -DS hello2

hello2: 文件格式 elf64-x86-64


Disassembly of section .interp:

0000000000000318 <.interp>:
318: 2f (bad)
319: 6c insb (%dx),%es:(%rdi)
31a: 69 62 36 34 2f 6c 64 imul $0x646c2f34,0x36(%rdx),%esp
321: 2d 6c 69 6e 75 sub $0x756e696c,%eax
326: 78 2d js 355 <__abi_tag-0x37>
328: 78 38 js 362 <__abi_tag-0x2a>
32a: 36 2d 36 34 2e 73 ss sub $0x732e3436,%eax
330: 6f outsl %ds:(%rsi),(%dx)
331: 2e 32 00 cs xor (%rax),%al

Disassembly of section .note.gnu.property:

0000000000000338 <.note.gnu.property>:
338: 04 00 add $0x0,%al
33a: 00 00 add %al,(%rax)
33c: 20 00 and %al,(%rax)
...

如使用交叉编译mips-linux-gnu-gcc hello.c进行编译链接,并直接使用objdump -DS进行反汇编,则会返回如下代码:

git@21371068:~/21371068/tools/readelf (lab1)$ objdump a.out -DS

a.out: 文件格式 elf32-big

objdump: can't disassemble for architecture UNKNOWN!

出现如上错误是因为,需要使用交叉编译链所对应的反汇编工具才能解析,在我们的实验环境下就是mips-linux-gnu-objdump

git@21371068:~/21371068/tools/readelf (lab1)$ mips-linux-gnu-objdump -DS hello.o

hello.o: 文件格式 elf32-tradbigmips


Disassembly of section .text:

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
...

objdump参数意义

-D  反汇编文件中的所有section(节)
-S 输出时按照C语言与汇编代码相对应的格式输出

Thinking 1.2

使用我们编写的readelf程序对内核文件检查后得到的视图如下:

image-20230312230359908

使用Linux系统内自带的readelf指令对readelf文件和hello文件进行分析(使用readelf -h readelf/hello指令)可以发现两个文件的类型不同:helloELF32类型,而readelfELF64类型

image-20230312230745084

image-20230312230826009

这说明我们的hello文件是32位的格式,而readelf则是64位的。我们打开readelf.c文件,发现其中的的数据类型前缀都是ELF32,也正是说明了这个程序负责分析32位的ELF文件。所以它不能分析身为64位格式程序的自己。

进入同目录下的Makefile文件查看,发现了两个文件在编译方式上的不同:

image-20230313000915321

查阅相关资料后得知参数-m32:编译出来的是32位程序,既可以在32位操作系统运行,又可以在64位操作系统运行。这也恰好印证了readelf指令对于这两个文件的类型判定。

补充:大小端转换

//小端->大端:
UINT EndianConvertLToB(UINT InputNum) {
UCHAR *p = (UCHAR*)&InputNum;
return(((UINT)*p<<24)+((UINT)*(p+1)<<16)+
((UINT)*(p+2)<<8)+(UINT)*(p+3));
}
//大端->小端
UINT EndianConvertBToL(UINT InputNum) {
UCHAR *p = (UCHAR*)&InputNum;
return(((UINT)*p)+((UINT)*(p+1)<<8)+
((UINT)*(p+2)<<16)+(UINT)*(p+3)<<24);
}

Thinking 1.3

在我们的实验中,系统启动被简化成了把内核加载到指定内存位置。

MIPS系统启动时首先接管的是bootloader,随后Linker Script把各个节映射到对应的段上,内核文件也在这时被加载到合适的地址空间中。

Exercise 1.2中,我们补全了kernel.lds文件,把.text.data.bss三个段映射到了合理空间

/* Exercise 1.2 Your code here. */

. = 0x80010000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
bss_end = .;
. = 0x80400000;
end = .;

经过Linker Script文件的引导,内核代码就会被加载到0x80010000这段地址。再通过ENTRY(_start)的入口规定,如此便保证了我们能够跳转到内核入口

Exercise 1.1

C语言指针

Exercise 1.1中,我们需要使用指针对ELF头进行寻址后取值,那么这时使用指针取得合适的地址就是重点了

C语言对指针的加法运算符进行了重载

如果使用了+对地址进行运算,地址的位移量会自动根据加号前的数据结构调整。

Struct st * p = (Struct st*)p + a

具体为:地址会向后移动a*sizeof(Struct st)字节

在我们的实验中需要在ELF头中寻找到节头表的入口,需要的行为是:

const void *sh_table = (Elf32_Shdr *)(binary + ehdr->e_shoff);
//binary、ehdr为ELF头地址;e_shoff为节头表入口偏移

但是有的同学写成了

const void *sh_table = (Elf32_Shdr *)(ehdr + ehdr->e_shoff);

原本的binary类型为const void *,它的加法运算符向后移动的单位为1字节;而已经转型为Elf32_Ehdrehdr重载后则会向后移动一个Elf32_Ehdr大小的地址空间。于是虽然两个指针指向了同一个地址,+后的值也相同,但是运算后得到的结果却截然不同

readelf.c文件的补全

实验目的为输出ELF文件的节头地址信息。

首先需要明确,我们需要的节头地址信息保存在节头表中每个项目的特定字段中(Elf32_Shdr -> sh_addr)。并且这个sh_addr指向ELF文件中的每个节头所在地址。那么就需要我们从ELF表头访问到节头表,并对每一项遍历即可。

遍历每一个节头的方法是:先读取节头的大小,随后以指向第一个节头的指针(即节头表第一项的地址)为基地址,不断累加得到每个节头的地址。

具体实现为根据Elf32_Edhr -> e_shoff寻找到节头表入口地址、根据Elf32_Edhr -> e_shnum获取节头表中所含有项的个数,并根据Elf32_Edhr -> e_shentsize获取节头表长度,便于位移

const void *sh_table;
Elf32_Half sh_entry_count;
Elf32_Half sh_entry_size;
/* Exercise 1.1: Your code here. (1/2) */
sh_table = (Elf32_Shdr *)(binary + ehdr->e_shoff);
sh_entry_count = ehdr->e_shnum;
sh_entry_size = ehdr->e_shentsize;
// For each section header, output its index and the section address.
// The index should start from 0.
for (int i = 0; i < sh_entry_count; i++) {
const Elf32_Shdr *shdr;
unsigned int addr;
/* Exercise 1.1: Your code here. (2/2) */
shdr = sh_table + i * sh_entry_size;
addr = (unsigned int)(shdr->sh_addr);
printf("%d:0x%x\n", i, addr);
}

Exercise 1.2

Linker Script

Linker Script中记录了各个节应该如何映射到段,以及各个段应该被加载到的位置。

Exercise 1.2中,我们就要利用Linker Script,对内核文件的各节进行内存指派,找到对应节的地址。段是由节所结合组成的,因为节的位置改变了,所以段的地址也会相应地发生移动,具体实现如下:

SECTIONS
{
/* fill in the correct address of the key sections: text, data, bss. */
/* Exercise 1.2: Your code here. */
. = 0x80010000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
bss_end = .;
. = 0x80400000;
end = . ;
}

其中的.号用作定位计数器,通过设置.的地址,声明接下来的节会被按序安放在该地址后。(在SECTIONS中,默认初始的地址为0地址,所以需要先修改地址然后再安排节文件)

后面的代码如.bss:{*(.bss)},表示将所有输入文件中的.bss节(右边
.bss)都放到输出的.bss节(左边的.bss)中。

观察kernel.lds的其他代码,还能发现这个文件规定了程序的入口地址。我们的实验程序通过ENTRY(_start)设置_start函数作为入口地址开始运行

/*
* Set the ENTRY point of the program to _start.
*/
ENTRY(_start)

_start函数被安放在/init/start.S文件中

Exercise 1.3

_start函数的设置

.text
EXPORT(_start)
.set at
.set reorder
/* disable interrupts */
mtc0 zero, CP0_STATUS
/* hint: you can reference the memory layout in include/mmu.h */
/* set up the kernel stack */
/* Exercise 1.3: Your code here. (1/2) */
li sp, 0x80400000
/* jump to mips_init */
/* Exercise 1.3: Your code here. (2/2) */
jal mips_init

mmu.h文件中我们可以查询到系统内核各部分内存分配情况,这里就能找到栈顶地址为0x80400000

设置结束后汇编程序完成,就可以跳转入C语言的函数入口mips_init

我们使用jalj指令进行函数的跳转。在不同文件链接时,链接器回对目标文件中的符号(包括函数名)进行重定位,修改跳向这些函数的地址,实现跨文件的函数调用

printk函数的实现

printk函数实现功能,底层上依靠的是console.c文件中printcharc函数对控制台进行字符的输出;

向上一层,print.c中的vprintfmt函数则通过格式化字符的形式对console.c中的函数进行合理调用,实现输出;

再向上一层,就是printk.c这个文件,它接收输出参数,并把变长参数表和传递给vprintfmt函数,最终实现字符的输出。

变长参数表

stdarg.h头文件对变长参数表定义了一系列宏变量与变量类型:

  • va_list:变长参数表对应的变量类型
  • va_start(va_list ap, lastarg):初始化变长参数表
  • va_arg(ca_list ap, 类型):去除变长参数表的下一个参数
  • va_end(va_list ap):结束变长参数表的使用

声明方式:

va_list ap;// 声明变长参数表
va_start(ap, lastarg);// 初始化参数表
int var = va_arg(ap, int);// 取出一个int类型的变量
...
va_end(ap);// 结束当前的变长参数表

回到我们的printk函数:printk本身接受了外部传入的不定长参数,创建了一个变长参数表,传入了vprintfmt函数

// printcharc
void printcharc(char ch) {
*((volatile char *)(KSEG1 + DEV_CONS_ADDRESS + DEV_CONS_PUTGETCHAR)) = ch;
}

// outputk
void outputk(void *data, const char *buf, size_t len) {
// buf:输出的字符串指针;len:输出的字符长度
for (int i = 0; i < len; i++) {
printcharc(buf[i]);
}
}

// vprintfmt
void vprintfmt(fmt_callback_t out, void *data, const char *fmt, va_list ap) { ... }
// 此处传入的out,实际上是outputk的函数指针,内部实际上在调用outputk函数进行输出

// printk
void printk(const char *fmt, ...) {
// fmt:传递的主字符串指针
va_list ap;
va_start(ap, fmt);
vprintfmt(outputk, NULL, fmt, ap);
va_end(ap);
}
// 自顶向下地:
// printk函数创建变长参数表,调用vprintfmt
// vprintfmt格式化参数表,调用outputk;
// outputk接收数据直接调用printcharc,实现功能

具体实现不再说明

心得体会

  1. 第一次完成lab1内容时还不能理解内核为什么能正常工作、函数为何能正常运行,只是按照指导书的说明进行补充而已。而恰是这个不理解,成了实验过程中的最大难题。只有完成内容后,再重新回过头来审视每一步,才能知道每一步的具体功效,明白实验过程中“那里”为什么要“那么做”。经常性的回顾对实验过程理解很有必要。
  2. 完成实验过程中并没有查看除了需要填写代码文件以外的文件,然而,各个函数的实现过程与相关信息其实都藏在课程组为我们写好的其他文件中,读过一遍其他函数的定义、调用与功能,才让实验过程中填写的代码有理可据。