实验内容:https://ipads.se.sjtu.edu.cn/courses/os/labs/lab1.html
环境配置
gcc降级
需要注意的是这个lab只能使用gcc4.8及以下版本编译,否则运行会有问题。
gcc降级的最简单的方法是将/usr/bin
目录下的gcc
删去,并做一个到gcc-4.8
的符号链接:
1 | cd /usr/bin |
如果没有安装gcc4.8,就先安装一下:
1 | sudo apt install gcc-4.8 |
32位依赖包的安装
在编译的时候如果出现__udivdi3 not found
或__muldi3 not found
的问题,可能是缺少依赖包导致的:
1 | sudo apt-get install gcc-multilib |
需要注意的是,如果问题依然存在,可能上述的依赖包是给gcc5以上使用的,安装给gcc4.8使用的依赖包即可:
1 | sudo apt install gcc-4.8-multilib |
qemu安装
这个没什么坑,按照tools说的做就行了
系统的启动流程
刚启动时,内存的布局是这样的(都是物理地址)
1 | +------------------+ <- 0xFFFFFFFF (4GB) |
启动的步骤大概是这样的:
- 从ROM中加载BIOS程序,从
0xffff0
开始执行 - BIOS将磁盘(或其他设备)的第一个扇区加载至
0x7c00
并执行(即boot loader,/boot/boot.S
) - boot loader 在完成硬件初始化、启动保护模式、加载kernel等任务后,将控制权转移到kernel(
/kern/entry.S
) - entry.S加载页表,设置esp、ebp,之后将控制权转移到init.c中的
i386_init
之后就是初始化显示设备、启动console之类的工作
启动过程中的注意点如下:
注意区别BIOS和boot loader,我之前一直把BIOS加载boot loader和boot loader加载kernel当作一回事,然后就十分僵硬
BIOS和boot loader 都经历了从实模式到保护模式的变化(i8086 => i386)
kernel的加载位置是0x10000,而entry的位置是0x10000c(根据ELF的格式,在加载完成后0x10018处的数据就是entry的地址)
页表在entrypgdir.c中,映射方式为:
- [0, 4MB) => [0, 4MB) 只读
- [KERNBASE + 0, KERNBASE + 4MB) => [0, 4MB)可写
因此在启用虚拟地址后依然可以使用物理地址取指
boot loader在磁盘的第一个sector,kernel从第二个sector开始(见/boot/main.c的注释)
ljmp和非本地跳转的longjump不是一回事
可变参数数量的函数
理论上,只需要caller将参数按照顺序压栈,callee按照首个参数的地址依次+4的顺序读内存,即可得到所有的参数,实现可变参数数量。
但是这样有两个问题:
- 需要知道参数的数据类型
- 需要知道参数的数量
stdarg.h对上述操作进行了一定程度的封装。
stdarg.h
stdarg.h中存在如下的宏定义:
1 |
以lab中的代码为例:
printf.c:25
1 | int |
printfmt.c:158
1 | case '*': |
其中va_start
使用第一个参数的地址初始化参数列表ap
,之后每次使用时传入ap
和参数类型,从中取出下一个参数。
不过这依然无法解决参数数量的预期和实际不一致的问题,下面的一些练习就与此相关。
Exercises
Exercise 3
At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
gdb记录如下:
1 | [ 0:7c2d] => 0x7c2d: ljmp $0x8,$0x7c32 |
说明引起变化的语句是
1 | # Jump to next instruction, but in 32-bit code segment. |
What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?
boot loader的最后一条是(/boot/main.c:60)
1 | ((void (*)(void)) (ELFHDR->e_entry))(); |
kernel的第一条是(/kern/entry.S:44)
1 | entry: |
How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?
ELF的头部提供了信息
Exercise5
Reset the machine (exit QEMU/GDB and start them again). Examine the 8 words of memory at 0x00100000 at the point the BIOS enters the boot loader, and then again at the point the boot loader enters the kernel. Why are they different? What is there at the second breakpoint? (You do not really need to use QEMU to answer this question. Just think.)
boot loader 将kernel加载到了这里
Exercise6
Trace through the first few instructions of the boot loader again and identify the first instruction that would “break” or otherwise do the wrong thing if you were to get the boot loader’s link address wrong. Then change the link address in
boot/Makefrag
to something wrong, runmake clean
, recompile the lab withmake
, and trace into the boot loader again to see what happens. Don’t forget to change the link address back andmake clean
afterwards!
即修改/boot/Makefrag:28
1 | $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o $@.out $^ |
0x7c00
修改为其它值后无法启动。BIOS钦定了0x7c00
来接管,就不要搞一个大新闻了
Exercise7
Use QEMU and GDB to trace into the JOS kernel and find where the new virtual-to-physical mapping takes effect. Then examine the Global Descriptor Table (GDT) that the code uses to achieve this effect, and make sure you understand what’s going on.
不知道怎么才能用gdb看GDT,据说要运行在ring0才行
What is the first instruction after the new mapping is established that would fail to work properly if the old mapping were still in place? Comment out or otherwise intentionally break the segmentation setup code in
kern/entry.S
, trace into it, and see if you were right.
接下来使用虚拟地址的地方就会出错,也就是这一句(/kern/entry.S:67):
1 | jmp *%eax |
这里源代码中也有一个小问题:
Now paging is enabled, but we’re still running at a low EIP(why is this okay?)
关于这一点我在系统的启动流程的注意点4中已经解释过。
Exercise8
We have omitted a small fragment of code - the code necessary to print octal numbers using patterns of the form “%o”. Find and fill in this code fragment. Remember the octal number should begin with ‘0’.
我一个OS的lab怎么第一项工作是做字符串处理?
参考16进制的写法就行了:
1 | case 'o': |
Exercise9.0
You need also to add support for the “+” flag, which forces to precede the result with a plus or minus sign (+ or -) even for positive numbers.
思路:如果读到‘+’,那么立一个flag:“需要显示‘+’”;再像平常那样读符号,如果是有符号整数且是正数,那么向输出流中放一个‘+’,再正常显示数字。
1 | case '+': |
Exercise9.1
Explain the interface between
printf.c
andconsole.c
. Specifically, what function doesconsole.c
export? How is this function used byprintf.c
?
这两者的接口为cputchar
,作用是往输出流中放入一个字符
Exercise9.2
Explain the following from
console.c
:
1
2
3
4
5
6
7 if (crt_pos >= CRT_SIZE) {
int i;
memcpy(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
crt_buf[i] = 0x0700 | ' ';
crt_pos -= CRT_COLS;
}
整体上移一行,并在新的一行中填入空格
Exercise9.3
For the following questions you might wish to consult the notes for Lecture 2. These notes cover GCC’s calling convention on the x86.
Trace the execution of the following code step-by-step:
1
2 int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
- In the call to
cprintf()
, to what doesfmt
point? To what doesap
point?
fmt
指向格式化字符串"x %d, y %x, z %d\n"
,ap
指向第一个可选参数x
Exercise9.4
Run the following code.
1
2 unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);What is the output? Explain how this output is arrived out in the step-by-step manner of the previous exercise
output:
1 | He110 World |
原因:
- 57616 == 0xe110
- 0x64 == ‘d’, 0x6c == ‘l’, 0x72 == ‘r’
The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set
i
to in order to yield the same output? Would you need to change57616
to a different value?
i需要改为0x726c6400
,57616
不需要改
Exercise9.5
In the following code, what is going to be printed after
y=
? (note: the answer is not a specific value.) Why does this happen?
1 cprintf("x=%d y=%d", 3);
一个在栈上相邻的值
Exercise9.6
Let’s say that GCC changed its calling convention so that it pushed arguments on the stack in declaration order, so that the last argument is pushed last. How would you have to change
cprintf
or its interface so that it would still be possible to pass it a variable number of arguments?
分析:
- 对于
cprintf
而言,需要知道fmt
的内容才能确定参数的数量 - 对于这一种传参方式,需要知道参数的数量才能确定第一个参数的位置
所以解决方案可能有:
- 新增一个参数,内容为参数数量,在参数表的末尾
- 只接受固定的两个参数,第一个为
fmt
,第二个为以NULL结尾的链表的首地址,链表中为参数,类似于char **argv
- 将可选参数放在前面,
fmt
在最后
Exercise10
Modify the function
printnum()
inlib/printfmt.c
to support"%-"
when printing numbers. With the directives starting with “%-“, the printed number should be left adjusted. (i.e., paddings are on the right side.) For example, the following function call:
1 cprintf("test:[%-5d]", 3), should give a result as
1 "test:[3 ]"(4 spaces after ‘3’). Before modifying
printnum()
, make sure you know what happened in functionvprintffmt()
.
思路:先统计输出的数字的位数(O(log n)
),然后输出数字,再补足空格
1 | if (padc == '-') { |
Exercise11
Determine where the kernel initializes its stack, and exactly where in memory its stack is located. How does the kernel reserve space for its stack? And at which “end” of this reserved area is the stack pointer initialized to point to?
/kern/entry.S:75
1 | # Set the stack pointer |
Exercise12
To become familiar with the C calling conventions on the x86, find the address of the
test_backtrace
function inobj/kern/kernel.asm
, set a breakpoint there, and examine what happens each time it gets called after the kernel starts. How many 32-bit words does each recursive nesting level oftest_backtrace
push on the stack, and what are those words?
发现每次调用esp缩小0x20,即8 words
Exercise13
Implement the backtrace function as specified above.
既然已经提供了read_ebp
函数,那么顺着ebp一路摸上去就能得到return address
和各个参数:
1 | ebp = (unsigned int *)read_ebp(); |
这些在ics的buffer overflow中见的多了。另一个问题是什么时候停止循环。在entry.S中发现:(entry.S:73)
1 | movl $0x0,%ebp # nuke frame pointer |
也就是说,初始化时ebp为0,当发现ebp为0(NULL)时即可停止循环。
Exercise14
Modify your stack backtrace function to display, for each
eip
, the function name, source file name, and line number corresponding to thateip
.
仿照上下文,添加:
1 | stab_binsearch(stabs, &lline, &rline, N_SLINE, addr); |
where do
__STAB_*
come from?
stab是gcc生成的调试信息,具体格式可以参考:GCC 生成的符号表调试信息剖析
Exercise15
In this exercise, you need to implement a rather easy “time” command. The output of the “time” is the running time (in clocks cycles) of the command. The usage of this command is like this: “time [command]”.
这个在大一暑假作业简易数据库的性能测试中就实现过一个类似,思路是:
- 记录开始时时间
- 运行
- 记录结束时时间
实现如下:
1 | int |
总结
这个lab需要写代码的部分只是真正的冰山一角,甚至大多能在上下文中找到提示。而对这些问题的思考才是这个lab最大的意义。