【mit6.828】Lab01
前言
- mit6.828每个实验的相关步骤以及自己的解读
- 参考lab中的介绍和其他博客的解读
1. Part1: PC Bootstrap
- lab01的相关知识的了解Lab 1 Part1
- Exercise 1
要求:熟悉汇编知识,至少能够阅读
- 安装qemu
- 下载&编译xv6系统
从官网或者博客给的地址将代码拖下来
对代码进行编译安装,可以看到进入了jos
(在进行安装&编译过程中,ubuntu20因为版本问题,会报错,解决方法)
- Exercise 2
- 要求:使用GDB对ROM BIOS的相关指令进行调试,并猜测其的运行(即熟悉指令的相关含义及对cpu的基本知识进行了解)
a. 使用make qemu-gdb
因为上一个jos打开没有关闭,用指令杀掉那个进程,[解决方法](https://blog.csdn.net/qq_36393978/article/details/118353939),重新对qemu进行编译
进入gdb调试场景,可以看到其已经执行了一条指令(但是并不是引导操作系统运行的指令)
更改 进入Lab文件中先执行make qemu-gdb 再另外开辟一个窗口执行make gdb,从而得到正确的引导界面
0xffff0: ljmp $0x3630,$0xf000e05b
0xfe05b: cmpw $0xffc8,%cs:(%esi)
0xfe062: jne 0xd241d0f1
0xfe066: xor %edx,%edx
0xfe068: mov %edx,%ss
0xfe06a: mov $0x7000,%sp
0xfe070: mov $0xfc63,%dx
0xfe076: jmp 0x5576cf6e
0xfcf6c: cli
-
0xffff0:ljmp 是进行一个长跳转,地址 f000e05b,通过这篇文章指出貌似是BIOS地址 x86架构启动
-
0xfe05b:cmpw 对两个操作数进行比较,其中cs寄存器为代码段寄存器,si寄存器存放着一个源地址。通过哦后面跳转逻辑,应该这里是做的一个地址判断。
-
0xfe062:jne 根据zf标志位来决定是否跳转,显然没有跳转。
-
0xfe066:xor:异或操作。这里就是讲dx寄存器进行清零操作
-
0xfe068:cli:进行中断关闭
0xfcf6e: mov %ax,%cx
0xfcf71: mov $0x8f,%ax
0xfcf77: out %al,$0x70
0xfcf79: in $0x71,%al
0xfcf7b: in $0x92,%al
0xfcf7d: or $0x2,%al
0xfcf7f: out %al,$0x92
0xfcf81: mov %cx,%ax
-
0xfcf6e:将ax寄存器的值,临时保存再cx寄存器中
-
0xfcf71、0xfcf77、0xfcf79:ax寄存器可以分为两部分使用,一个是al和ah寄存器。这里将8f传给ax寄存器,即对al寄存器的赋值。 in、out指令是控制硬件设备输入输出的指令,通过out将al寄存器的值传到0x70上。其中0x70、0x71对应的CMOS设备,这两个操作就是在对CMOS芯片做前期控制处理。
-
0xfcf7b、0xfcf7d、0xfcf7f:0x92对应的PS/2X系统端口,同样是在进行相关初始化控制
-
0xfcf81:将ax的还原
0xfcf84: lidtl %cs:(%esi)
0xfcf8a: lgdtl %cs:(%esi)
0xfcf90: mov %cr0,%ecx
0xfcf93: and $0xffff,%cx
0xfcf9a: or $0x1,%cx
0xfcf9e: mov %ecx,%cr0
- 0xfcf84、0xfcf8a:对中断向量表和全局描述符寄存器进行加载,但是不清楚这里都是加载到同一个寄存器中的
- 0xfcf90、0xfcf93、0xfcf9a、0xfcf9e:对cr0寄存器处理CPU寄存器,其中cr0寄存器是cpu中的控制寄存器,这里是在操作cpu的运行模式,修改cr0的第0位
关于启动代码的阅读,到这里就为止了,通过上面的流程,我们可以基本了解到,在机器启动时,先启动BIOS,通过BIOS来对底层的输入设备进行初始化控制处理,然后再是Boot Loader,最后对操作系统进行加载。
2. Part 2:The Boot Loader
要求:设置使用GDB调试,从0x7c00处打上断点,进行调试。使用反汇编,来源码和反汇编文件的区别,同时跟踪main.c中的readsect(),取找到其对应的汇编指令。
重新启动 gdb,在0x7c00处打上断点,然后让其continue
b *0x7c00 //打断点
c //继续执行
[ 0:7c00] => 0x7c00: cli //跳转到指定地址处
- boot.S
在进行下面的指令执行之前,先来阅读boot.S中的代码
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
所以boot.S会将这段代码拿出来并从0x7c00在实模式下执行。实现启动CPU转换为保护模式,执行c代码
# 将所有关闭中断
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# Set up the important data segment registers (DS, ES, SS).
# 重新对相关寄存器值重新赋值
xorw %ax,%ax # Segment number zero
movw %ax,%ds # -> Data Segment
movw %ax,%es # -> Extra Segment
movw %ax,%ss # -> Stack Segment
... ...
# 开始启动c
# Set up the stack pointer and call into C.
movl $start, %esp
call bootmain
boot.S:通过BIOS对CPU进行相关处理,将其运行模式从实模式转换到保护模式来,完成后启动main.c函数
- main.c
代码中的简介
/**********************************************************************
* This a dirt simple boot loader, whose sole job is to boot
* an ELF kernel image from the first IDE hard disk.
* DISK LAYOUT
* * This program(boot.S and main.c) is the bootloader. It should
* be stored in the first sector of the disk.
* * The 2nd sector onward holds the kernel image.
* * The kernel image must be in ELF format.
* BOOT UP STEPS
* * when the CPU boots it loads the BIOS into memory and executes it
* * the BIOS intializes devices, sets of the interrupt routines, and
* reads the first sector of the boot device(e.g., hard-drive)
* into memory and jumps to it.
* * Assuming this boot loader is stored in the first sector of the
* hard-drive, this code takes over...
* * control starts in boot.S -- which sets up protected mode,
* and a stack so C code then run, then calls bootmain()
* * bootmain() in this file takes over, reads in the kernel and jumps to it.
**********************************************************************/
通过上面简介,可以知道关于disk上的分布及bios的作用。main.c就是boot loader,它将负责将操作系统引入到内存中
void
bootmain(void)
{
struct Proghdr *ph, *eph;
// read 1st page off disk
// 读取硬盘中的第一页内容【起始地址,长度信息】
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// is this a valid ELF?
// 检测ELF的有效性
if (ELFHDR->e_magic != ELF_MAGIC)
//出问题将跳转
goto bad;
// load each program segment (ignores ph flags)
// 获取头部的信息表格Header Table,ph获取的为首地址信息
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;//说明这里读取的是这段内存的大小,下面就开始读取
for (; ph < eph; ph++)
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// call the entry point from the ELF header
// note: does not return!
// 这里引导内核进入
((void (*)(void)) (ELFHDR->e_entry))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
}
main.c的功能,读取硬盘中第一页的ELF文件,进行相关有效性检测并根据ELF文件加载数据,引导内核进入。
- gdb调试
上面对相关文件进行了了解,下面开始调试做exercise
反汇编
通过gdb中的反汇编指令,可以发现在执行boot.S时,反汇编的结果基本都相同,不同点在于标号被翻译成了真实的地址
readseg
在jos文件夹下的obj/boot有对boot_loader的汇编文件,
00007d25 <bootmain>:
{
7d25: f3 0f 1e fb endbr32
7d29: 55 push %ebp
7d2a: 89 e5 mov %esp,%ebp
# 注意:bp、sp为堆栈寄存器,在函数开始时都会调用
7d2c: 56 push %esi
7d2d: 53 push %ebx
...
...
for (; ph < eph; ph++)
7d6b: 83 c3 20 add $0x20,%ebx
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
7d6e: ff 73 e4 pushl -0x1c(%ebx)
7d71: ff 73 f4 pushl -0xc(%ebx)
7d74: ff 73 ec pushl -0x14(%ebx)
7d77: e8 66 ff ff ff call 7ce2 <readseg>
for (; ph < eph; ph++)
7d7c: 83 c4 10 add $0x10,%esp
7d7f: eb e5 jmp 7d66 <bootmain+0x41>
((void (*)(void)) (ELFHDR->e_entry))();
7d81: ff 15 18 00 01 00 call *0x10018
}
}
这部分的目的,就在于对指令执行和反汇编的了解,可以通过这个流程了解汇编语言是如何执行一个c函数的(反汇编文件中都是一句话对应一段汇编)
在bootmain的最后一行,
call *0x10018
即意味着开始引入系统内核
3. Part 3:The Kernel
Exercise 7
使用调试测试0x00100000 和 0xf0100000.地址的内容。
注释movl %eax, %cr0会出现什么情况
这个exercise中提到了一个entry.S文件,在这个文件夹下还存在要给entrypgdic.c的文件,这个文件是在引导操作系统进来时,c代码运行的文件是【KERNBASE+1MB】,所以在此之前要实现一个地址机制的转换,我们能运行的一个虚拟内存转换机制.
// The entry.S page directory maps the first 4MB of physical memory
// starting at virtual address KERNBASE (that is, it maps virtual
// addresses [KERNBASE, KERNBASE+4MB) to physical addresses [0, 4MB)).
// We choose 4MB because that's how much we can map with one page
// table and it's enough to get us through early boot. We also map
// virtual addresses [0, 4MB) to physical addresses [0, 4MB); this
// region is critical for a few instructions in entry.S and then we
// never use it again.
//
// Page directories (and page tables), must start on a page boundary,
// hence the "__aligned__" attribute. Also, because of restrictions
// related to linking and static initializers, we use "x + PTE_P"
// here, rather than the more standard "x | PTE_P". Everywhere else
// you should use "|" to combine flags.
表明该虚拟地址的转换机制能转换的空间只有4MB,可以从高地址转也可以从低地址,但是最后转换到物理地址是唯一的【0,4MB】(0x00000000~0x00400000)
- 测试
b *0x10000C
# 打上断点,这里是entry.S的入口地址,也是bootloader最后call的地址
# 查看两个寄存器的内容
(gdb) x/4xb 0x00100000
0x100000: 0x02 0xb0 0xad 0x1b
(gdb) x/4xb 0xf0100000
0xf0100000 <_start-268435468>: Cannot access memory at address 0xf0100000
# 显示无法访问
继续运行代码,启动了地址转换机制后,继续查询,两个地址下的内容是相同的
(gdb) x/4xb 0xf0100000
0xf0100000 <_start-268435468>: 0x02 0xb0 0xad 0x1b
(gdb) x/4xb 0x0100000
0x100000: 0x02 0xb0 0xad 0x1b
可以看到经过转换后,两个地址下的内容相同
后面将movl %eax, %cr0进行注释,查看结果
=> 0xf010002c <relocated>: Error while running hook_stop:
Cannot access memory at address 0xf010002c
relocated () at kern/entry.S:74
74 movl $0x0,%ebp
# 这里表明地址转换失败0xf010002c无法访问
Exercise 8
补全指定文件夹下中的代码(通过补全这个代码,了解相关代码,
使得对printf的底部实现进行了解),我们需要阅读kern/printf.c,lib/printfmt.c和kern/console.c
找到需要替换的代码进行替换,在lib/printmt.c
void vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)
{
...
case 'o':
putch('0', putdat);
num = getuint(&ap, lflag);
base = 8;
goto number;
...
}
这个接口 就是用来显示数字字符的,而我们填充的就是如何显示八进制字符,参照前面字符显示的方式,就可以填写了
问题1:Explain the interface between printf.c and console.c. Specifically,
what function does console.c export?
How is this function used by printf.c?
在printf.c中使用到console的接口是cputchar,cputchar会将获取的字符串读入
void cputchar(int c)
{
//这里又调用cons_puts(c)
cons_putc(c);
}
// output a character to the console
static void cons_putc(int c)
{
//获取到字符串后,就对相关的硬件端口进行初始化,然后将字符串输出到相关显示器上
//后面一道题的时候,会
serial_putc(c);
lpt_putc(c);
cga_putc(c);
}
问题2:Explain the following from console.c:
1 if (crt_pos >= CRT_SIZE) {
2 int i;
3 memmove(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
4 for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
5 crt_buf[i] = 0x0700 | ' ';
6 crt_pos -= CRT_COLS;
7 }
/**
crt_buf:需要显示的字符缓冲区
crt_pos:字符位置
#define CRT_ROWS 25
#define CRT_COLS 80
CRT_SIZE:(CRT_ROWS * CRT_COLS),应该就表示显示器的位置大小 25*80
memmove表示字符串的复制
*/
所以这段代码的意思就是如果crt_buf中的数据,一页屏幕装不下,就需要向上移动一下,留出一列来,并空出来显示。
问题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:
int x = 1, y = 3, z = 4;
cprintf("x %d, y %x, z %d\n", x, y, z);
1.
In the call to cprintf(), to what does fmt point? (调用cprintf时,fmt指向的什么,ap只想的什么)
To what does ap point?
2.
List (in order of execution) each call to cons_putc, va_arg,
and vcprintf. For cons_putc, list its argument as well.
For va_arg, list what ap points to before and after the call.
For vcprintf list the values of its two arguments.(运行代码,查看consputs的参数、va_arg在调用前后的参数变化,vcprint的两个参数值)
要回答这个问题,就先找到cprinf的代码
int cprintf(const char *fmt, ...){
va_list ap;
int cnt;
va_start(ap, fmt);
cnt = vcprintf(fmt, ap);
va_end(ap);
return cnt;
}
va_list是解决c语言中指定可变参数的在exercise中,可以明显发现fmt指定的字符串,ap代表后面的n,m
回答第二个问题
添加相关代码,然后重新编译,在kernel.asm找到地址,打上断点
Lab1_exercise8_3:
cprintf("x %d, y %x, z %d\n", x, y, z);
f01000d9: 6a 04 push $0x4
f01000db: 6a 03 push $0x3
之前做实验,记得给虚拟地址转换取消掉,不然....
调试进入断点处
进一步进入函数内部
可以发现fmt指向的地址,然后查看对应地址的内容可以发现fmt中存储的字符串
Exercise 4
运行:
unsigned int i = 0x00646c72;
cprintf("H%x Wo%s", 57616, &i);
最终输出是hello world,这是因为前面输出的格式第一个为%x,而对应的数字为57616,对其进行十六进制显示,第二个参数则是显示i存储的值,就是对i相应值进行一个字符变换
通过这个exercise,可以简单理解printf的底层实现。printf的两个参数分别装入fmt,ap中,底层是对其进行读取,同时对fmt进行遍历,当遍历出现“%”时,就进入另一个循环对对fmt中%后的字符进行识别,然后读取ap中的参数,按要求进行显示,显示中会对各个显示需要的端口进行配置,同时会在显示时进行检测,当字符数大于了屏幕大小,就需要做出调整。
3.1 Stack
这一节。,讨论堆栈,在lab中单独开了一节,将讨论堆栈是如何被使用的
问题9
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?
(哪条指令开始初始化堆栈空间的,内存地址又在哪个地方,如果给堆栈开辟空间的)
在引导bootloader之前,会先在entry.S文件做相关初始化,在正式引导系统前,可以看到对堆栈的初始化
可以通过反汇编文件,也可以通过调试发现最后送入esp的地址
在entry.S中为bootstacktop设置了大小KSTKSIZE,这就是初始化基于堆栈的空间大小。而堆栈向下生长,则初始化指针只想栈顶处
在lab1 中简单提到,%esp寄存器指向整个堆栈的正在使用部分的最低处,其下是未被使用的堆栈,ebp则记录每个程序中栈帧相关的重要寄存器。
通过对obj/kern/kernnel.asm中的test_backtrace进行调试,
来探索c程序在机器是如何执行的
找到test_backtrace函数
可以发现这是一个嵌套函数,我们可以通过调试也可以通过反汇编程序去看看其是怎么执行的
test_backtrace在kern/init.c中被执行
通过反汇编程序找到对应的地址,然后打上断点
进入单步调试
在进一步进入堆栈之前,其会进行一些堆栈操作
push %ebp,因为进入了一个新的函数,就要将上一个函数的栈帧底部进行保存,在还未进入当前test_backtrace之前,要对上一个函数做一些保存处理,然后将当前栈帧顶esp的值传给ebp(这样做当该程序执行结束后,就需要拿回调用函数的指针)
当在test_backtrace中调用其他函数时,都会使用sub 对esp进行操作,这是为了给其他调用的子程序函数预留参数空间,同时栈向下生长。当调用另一个子程序后,就又按test_backtrace刚进入一样对栈帧做一些处理。当函数执行结束,ebp从栈中弹出,esp指向ebp,即回到调用函数(如果存在返回值,则使用寄存器将这些变量临时进行保存)
4. 小结
-
通过Lab 01学习到操作系统启动的整体流程:BIOS(进行硬件端口的相关初始化)--->启动boot loader(在从disk调用os代码前,先对disk中第一页上的一些EFI信息进行安全性检测)--->entry.S(调用C程序之前,还需要做预处理,操作系统中的地址寻址方式和CPU的寻址方式有差别,所以在之前需要做一个地址转换,entry.S会启动一个设计的页表,在启动os之前使用一些,以实现os启动后的地址转换)
-
prinf的实现:printf的两个参数分别装入fmt,ap中,底层是对其进行读取,同时对fmt进行遍历,当遍历出现“%”时,就进入另一个循环对对fmt中%后的字符进行识别,然后读取ap中的参数,按要求进行显示,显示中会对各个显示需要的端口进行配置,同时会在显示时进行检测,当字符数大于了屏幕大小,就需要做出调整。注意fmt的指向对象,和ap的具体使用
-
函数中堆栈的使用:进行函数调用主要还是依靠esp和ebp,esp指向当前的栈帧顶,ebp则指向一个函数的基址。在函数进行调用时,会将ebp放进栈中,当前的esp值作为ebp的新值,同时调用函数在调用子函数时,会对esp进行减操作进行移动(因为地址是从高位移向低位)为子函数参数预留空间,因为调用时,会先将参数装入(从右向左装入),再进入函数体。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律