Lab01
思考题
thinking1.1
参数
-D : Display assembler contents of all sections
,即反汇编所有节的内容
-S : Intermix source code with disassembly
,显示与反汇编结合的源代码
编译与反汇编Ⅰ
main.c
1 |
|
反汇编main.o(只截取main代码段)
1 |
|
反汇编main(只截取main代码段)
1 |
|
在链接时进行了重定位,main
地址不再是0,而是0x4000b0。
编译与反汇编Ⅱ——反汇编vmlinux
1 |
|
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,禁用全局中断。
将CP0
的CONFIG
寄存器中值取出,将低三位置0又将第2位置1,则低三位K0区值为2,这时意为决定kseg0区不用高速缓存。
实验难点
难点一 ELF文件的解析
1 |
|
这是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
中是仿真器的位置
理解以后自然觉得不难,这样梳理以后就会对整体结构清晰很多。