Exercise 3, 在地址 0x7c00
处设下端点继续执行
-
At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
ljmp $PROT_MODE_CSEG, $protcseg
-
What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?
7d71: ff 15 18 00 01 00 call *0x10018
-
Where is the first instruction of the kernel?
movw $0x1234,0x472
-
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?
ph->p_memsz
Exercise 3
#include
#include void f(void) { int a[4]; int *b = malloc(16); int *c; int i; printf("1: a = %p, b = %p, c = %p\n", a, b, c); c = a; for (i = 0; i < 4; i++) a[i] = 100 + i; c[0] = 200; printf("2: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n", a[0], a[1], a[2], a[3]); c[1] = 300; *(c + 2) = 301; 3[c] = 302; printf("3: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n", a[0], a[1], a[2], a[3]); c = c + 1; *c = 400; printf("4: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n", a[0], a[1], a[2], a[3]); // 输出200 400 301 302 // 现在a数组的字节分布为(小端)C8000000 90010000 2D010000 2E010000(00C8 0190 012D 012E) // c指向a[1] c = (int *) ((char *) c + 1); // 将c先转换为char指针指向下一个字节后再转回int指针 *c = 500; // C8000000 90*010000 2D*010000 修改 * 号里面的四个字节 // 500 -> F4010000 // a-> C8000000 90F40100 00010000 2E010000 // 输出200 128144 256 302 printf("5: a[0] = %d, a[1] = %d, a[2] = %d, a[3] = %d\n", a[0], a[1], a[2], a[3]); b = (int *) a + 1; c = (int *) ((char *) a + 1); printf("6: a = %p, b = %p, c = %p\n", a, b, c); //a = 0x7ffeebb28200, b = 0x7ffeebb28204, c = 0x7ffeebb28201 //可以看到int指针+1 from c to b 是 4 个自己 而 char 指针只是一个字节 } int main(int ac, char **av) { f(); return 0; }
Exercise 6
i386-elf-objdump -f obj/kern/kernel
obj/kern/kernel: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c
看到内核的第一条程序的入口是 0x1000c
这里有个问题不是很明白, boot.asm
中,call 的最后一条程序的地址为此为啥会跳到knernal呢?
7d71: ff 15 18 00 01 00 call *0x10018
Part 3: The Kernel
i386-elf-objdump -x obj/kern/kernel | grep -2n LOAD
可以看见 text 的 link address("VMA")-> f0100000 与 load address ("LMA") 00100000 有所不同。
15-Idx Name Size VMA LMA File off Algn
16- 0 .text 0000171e f0100000 00100000 00001000 2**2
17: CONTENTS, ALLOC, LOAD, READONLY, CODE
这时因为操作系统常常会被链接到很高的虚拟地址(eg. f0100000 ) 为了原理用户程序会使用到的地址空间。然而有许多的机器并没有0xf0100000
(3.7GB) 这么大的内存,所以我们需要处理器的内存管理器讲这个地址map 到 0x00100000 (kernel 被 load 的地址。
Exercise 7.
b *0x0010000c #在内核的第一条指令处放下一个断点
si #进行单步调试
(gdb) b *0x10000c
Breakpoint 1, 0x0010000c in ?? ()
(gdb) si
=> 0x100015: mov $0x110000,%eax
0x00100015 in ?? ()
(gdb) si
=> 0x10001a: mov %eax,%cr3
0x0010001a in ?? ()
(gdb) si
=> 0x10001d: mov %cr0,%eax
(gdb) si
=> 0x100020: or $0x80010001,%eax
0x00100020 in ?? ()
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x00000000 0x00000000 0x00000000 0x00000000
0xf0100010 : 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) si
=> 0x100025: mov %eax,%cr0
0x00100025 in ?? ()
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x00000000 0x00000000 0x00000000 0x00000000
0xf0100010 : 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) si
=> 0x100028: mov $0xf010002f,%eax
0x00100028 in ?? ()
(gdb) x/8x 0xf0100000
0xf0100000 <_start+4026531828>: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766
0xf0100010 : 0x34000004 0x0000b812 0x220f0011 0xc0200fd8
可以看见在当内核执行了 mov $0xf010002f,%eax
这个指令之后,0xf0100000
地址开始有值。==这里不是很明白这个地址的意思==
Comment out the movl %eax, %cr0
in kern/entry.S
, trace into it, and see if you were right. 导致没有分页会让整个程序卡死。
Exercise 8.
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.
num = getuint(&ap, lflag);
base = 8;
goto number;
- Specifically, what function does
console.c
export? How is this function used byprintf.c
?
static void
putch(int ch, int *cnt)
{
cputchar(ch);
*cnt++;
}
用来在 console 中输出字符。
- Explain the following from
console.c
if (crt_pos >= CRT_SIZE)
{
int i;
memmove(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;
}
如果crt_pos 超过了crt_buf的最大值,将 crt_buf 的 1-n-1 移动到 0-n-2
再将下标为 n-1 的那行全部置为空格,属性设置为0x0700
参数
int c
是什么,0xff
和0x0700
又是什么?
int c
一共32bit,其中高16位用来表示属性,低16位用来表示字符。因此,与0xff
作 and 运算就是去掉属性,只看字符内容。与~0xff
作 and 运算就是去掉字符,只看属性。与0x0700
作 or 运算就是设为默认属性。
==这部分是什么意思我还没有弄懂==。后面再看看是什么意思。
-
fmt 指向的是 "x %d, y %x, z %d\n"
(gdb) x/s fmt 0xf0101a6e: "x %d, y %x, z %d\n
vcprintf()
vcprintf (fmt=0xf0101a6e "x %d, y %x, z %d\n", ap=0xf010ff04 "\001")
putch (ch=120, cnt=0xf010fecc)
cons_putc('x')
cons_putc(' ')
va_arg()
cons_putc('1')
cons_putc(',')
cons_putc(' ')
cons_putc('y')
cons_putc(' ')
va_arg()
cons_putc('3')
cons_putc(',')
cons_putc(' ')
cons_putc('z')
cons_putc(' ')
va_arg()
cons_putc('4')
cons_putc('\n')
可以用下面这个命令将相关的函数从文件中找到并且生成断点的命令:进行debug。
grep -HnE "cons_putc|va_arg|vcprintf" lib/printfmt.c kern/printf.c | cut -f1,2 -d ":" | xargs -I {} echo break {}
- 57616 = 0xe110。此外,根据x86的小端序,&i指向了byte序列0x72、0x6c、0x64、0x00。这等同于字符串”rld”。所以,最终的输出为”He110 World”。
The Stack
x86 的栈指针(esp)指向被使用栈的最低位置(因为栈底在地址的高位。)
关于栈的相关信息可以参考:
- C函数调用过程原理及函数栈帧分析.
- C语言函数调用栈
设置断点
b kern/init.c:12
b kern/init.c:16
b kern/init.c:20
#调用test_backtrace时
lea -0x1(%ebx), %eax #eax = ebx - 1 -> x-1
push %eax #被调函数param压栈 从右往左
call 0xf0100040 #call 标号(将当前的IP压栈后,转到标号处执行指令)
#等效于:
#pushl %eip 会在函数返回是通过 ret 将其出栈
#movl f, %eip
#执行 call 指令后 esp - 1
#esp 0xf010ff80 0xf010ff80
#esp 0xf010ff7c 0xf010ff7c
#eip 指向当前函数调用
#eip 0xf0100074 0xf0100074
#eip 0xf01008e3 0xf01008e3
#进入test_backtrace时
push %ebp #主调函数帧基指针EBP
mov %esp,%ebp # 将父函数的栈顶作为被调函数的栈底
push %ebx
sub $0xc,%esp #将栈顶指针向下移动 12 字节(3wds) 在栈上开辟一个空间存储局部变量,注意这里用的是 sub(改变ESP值来为函数局部变量预留空间)
mov 0x8(%ebp),%ebx #将ebx 设置为 ebp + 8
#test_backtrace return时
mov -0x4(%ebp),%ebx
leave
#等效于
#movl %ebp, %esp
#popl %ebp
ret
#等效于
#popl %eip
根据惯例,寄存器%eax
、%edx
和%ecx
为主调函数保存寄存器(caller-saved registers),当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。
寄存器%ebx
、%esi
和%edi
为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。
(gdb) bt
#0 test_backtrace (x=0) at kern/init.c:18
#1 0xf0100068 in test_backtrace (x=1) at kern/init.c:16
#2 0xf0100068 in test_backtrace (x=2) at kern/init.c:16
#3 0xf0100068 in test_backtrace (x=3) at kern/init.c:16
#4 0xf0100068 in test_backtrace (x=4) at kern/init.c:16
#5 0xf0100068 in test_backtrace (x=5) at kern/init.c:16
#6 0xf01000d4 in i386_init () at kern/init.c:39
#7 0xf010003e in relocated () at kern/entry.S:80
bt 命令可以看见堆栈的情况
通过 x/20x $esp
命令可以看到从栈顶往下走的情况其中 callee (被调函数)的返回指令为 0xf0100068 是在 caller 的ebp 下面。所以通过 *(ebp+1)
就可以将起读出来。
0x00000001 <- caller ebx
0xf010ff58 <- caller ebp
0xf0100068 <- add $0x10,%esp
0x00000000 <- callee param
0x00000001
0xf010ff78
0x00000000
0xf0100882
0x00000002 <- caller ebx
0xf010ff78 <- caller ebp
0xf0100068 <- add $0x10,%esp(epi)
0x00000001 <- callee param
增加的代码如下
uint32_t ebp,*args;
cprintf("Stack backtrace:\n");
ebp = read_ebp();
while (ebp != 0)
{
args = (uint32_t *)ebp;//将 ebp 的值转换为指针地址
ebp = args[0]; //将 caller 的 ebp 传给这个参数
cprintf("\tebp %x eip %x args %08x %08x %08x %08x %08x\n",ebp, args[1], args[2], args[3], args[4],args[5], args[6]);
}
Exercise 12:
这里需要知道 stab
是什么东西。可以参考这个文章: STAB 格式。
// Hint:
// There's a particular stabs type used for line numbers.
// Look at the STABS documentation and to find
// which one.
// Your code here.
stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
if (lline <= rline)
{
info->eip_line = stabs[lline].n_desc;
}
else
{
return -1;
}
static struct Command commands[] = {
{"help", "Display this list of commands", mon_help},
{"kerninfo", "Display information about the kernel", mon_kerninfo},
{"backtrace", "Display information about the kernel", mon_backtrace}};
running JOS: (1.5s)
printf: OK
backtrace count: OK
backtrace arguments: OK
backtrace symbols: OK
backtrace lines: OK
Score: 50/50
总结:
至此,我已经看了这个lab1快有一周的时间了,由于关于C语言, 汇编的许多东西我都没有特别明白所以看起来非常的吃力。
技术总结:
- 电脑上电之后开始载入 BIOS, BIOS 开始搜索外设寻找 bootable 的外设,如果 bootable, BIOS 将会把 OS 的 bootloabder 载入到 0x7c00~ 0x7dff 并且将 CS:IP 7c00.
- OS 的bootloabder 开始加载核心。首先是是通过一系列的汇编语言开启CPU的保护模式,然后调用 bootmain() 函数。bootmain() 函数通过读取磁盘将 ELF 格式的 kernel 载入到内存中,并且将 CS:IP 跳转到 ELF 的 enty 地址开始运行核心代码。
- Kernel 将启动分页模式将低地址的代码(0x00100000) 映射到(0xf0100000) 高位地址。
- 之后实验内容让我们阅读了 cprintf (类似 printf)的代码,并且完善了其实现。主要的思想是根据 % 后面的symbol来确获取定不定变量的
va_arg
函数的传入类型,从而通过不同的指针获取不同的数据类型。因为指针类型的不同,同样的数据会被翻译成不同的结果。 - 接下来是函数调用的过程(怎么压栈,以及栈的相关知识)。
- 最后是学习 backtrace 相关的东西,对程序保存时输出的信息有了一点了解。
参考文章
- 6.828 操作系统 lab1 实验报告
- https://github.com/clpsz/mit-jos-2014/tree/master/Lab1/Exercise1
- MIT6.828操作系统工程Lab1-Booting a PC实验报告