MIT 6.828 Lab 1: Booting a PC
课程地址:6.828 Operating System Engineering
MIT 6.828 Operating System Engineering的大作业共由6个Lab组成,这是其中第一个。这6个Lab组合在一起便是MIT用于教学的操作系统jos。在每个Lab中,学生会被要求实现某些功能。
Lab 1 Overview:
准备工作
准备工作包括依赖环境安装与源码克隆。我的开发环境是64-bit Ubuntu 18.04。
依赖环境安装
为了安全以及调试方便等原因,32-bit jos将运行在QEMU模拟器中。为了编译与测试内核,还需要安装Compiler Toolchain(汇编器,连接器,C编译器)以及调试器。
Compiler Toolchain
Linux系统一般提供了本课程所需的Toolchain。使用以下两条命令查看Toolchain是否安装
% objdump -i
输出第2行应有 elf32-i386 字样。
% gcc -m32 -print-libgcc-file-name
输出应类似 /usr/lib/gcc/i486-linux-gnu/version/libgcc.a 或 /usr/lib/gcc/x86_64-linux-gnu/version/32/libgcc.a。
在64位机器上,还需安装32-bit支持库。在Ubuntu Linux上,输入以下命令
% sudo apt-get install gcc-multilib
QEMU模拟器
课程设计者对于官方QEMU模拟器打了补丁使得其更好地工作,遵循以下步骤来安装补丁版本。
- 克隆课程补丁版本IAP 6.828 QEMU git仓库(可能会较慢):
git clone https://github.com/mit-pdos/6.828-qemu.git qemu
- 在Linux上,还需安装以下库:
% sudo apt-get install libsdl1.2-dev libtool-bin libglib2.0-dev libz-dev libpixman-1-dev
- 配置源代码:
./configure --disable-kvm --disable-werror [--prefix=PFX] --target-list="i386-softmmu x86_64-softmmu"
prefix参数可选,PFX用于指明QEMU安装路径。默认为/usr/local。
- 使用
make && make install
进行安装。
源码克隆
非常简单。在这里我遵循官方教程。
1 mkdir ~/6.828 2 cd ~/6.828 3 git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab 4 cd lab
现在,Lab所需的代码就已经clone至~/6.828/lab文件夹下了。接着测试内核是否能正常启动,在lab文件夹中键入make命令:
% make + as kern/entry.S + cc kern/entrypgdir.c + cc kern/init.c + cc kern/console.c + cc kern/monitor.c + cc kern/printf.c + cc kern/kdebug.c + cc lib/printfmt.c + cc lib/readline.c + cc lib/string.c + ld obj/kern/kernel + as boot/boot.S + cc -Os boot/main.c + ld boot/boot boot block is 380 bytes (max 510) + mk obj/kern/kernel.img
然后键入make qemu-nox命令:
% make qemu-nox Booting from Hard Disk... 6828 decimal is XXX octal! entering test_backtrace 5 entering test_backtrace 4 entering test_backtrace 3 entering test_backtrace 2 entering test_backtrace 1 entering test_backtrace 0 leaving test_backtrace 0 leaving test_backtrace 1 leaving test_backtrace 2 leaving test_backtrace 3 leaving test_backtrace 4 leaving test_backtrace 5 Welcome to the JOS kernel monitor! Type 'help' for a list of commands. K>
在这里也可以键入make qemu命令,效果是相同的,只不过没有-nox后缀的命令会打开一个QEMU Window,这需要图形化界面支持。而我使用ssh连接服务器,只能使用带有-nox后缀的命令。现在的内核非常简陋,只有两条命令可以执行:help与kerninfo。
启动与调试
启动过程在前一部分已经介绍过,这里不再赘述。在进行调试之前,首先了解一下PC的内存空间
+------------------+ <- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | | | Unused | | | +------------------+ <- depends on amount of RAM | | | | | Extended Memory | | | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | | | +------------------+ <- 0x00000000
注意BIOS ROM,它占据了 0x000F0000 到 0x000FFFFF 的64KB空间。
The BIOS is responsible for performing basic system initialization such as activating the video card and checking the amount of memory installed. After performing this initialization, the BIOS loads the operating system from some appropriate location such as floppy disk, hard disk, CD-ROM, or the network, and passes control of the machine to the operating system.
The ROM BIOS
Exercise 2. Use GDB's si (Step Instruction) command to trace into the ROM BIOS for a few more instructions, and try to guess what it might be doing. You might want to look at Phil Storrs I/O Ports Description, as well as other materials on the 6.828 reference materials page. No need to figure out all the details - just the general idea of what the BIOS is doing first.
在这一节我们会对内核进行调试来观察BIOS的工作过程。在lab文件夹下分别打开两个终端。一个终端键入make qemu-nox-gdb,这会启动QEMU,不过QEMU会在执行第一条指令前停止并等待GDB的接入。此时在另一个终端键入make gdb,就会进入debug模式。可以得到如下输出:
1 The target architecture is assumed to be i8086 2 [f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b 3 0x0000fff0 in ?? () 4 + symbol-file obj/kern/kernel 5 (gdb)
Line 2为下一条指令(这里也是第一条指令)。此时我们有CS=0xf000,IP=0xfff0。容易看出,这条指令位于BIOS ROM空间的顶端。现在需要跳转至BIOS ROM中间一点的位置,即CS=0xf000,IP=0xe05b。使用si命令单步调试,使用info reg查看寄存器内容。
(gdb) si [f000:e05b] 0xfe05b: cmpl $0x0,%cs:0x6ac8 0x0000e05b in ?? () (gdb)
使用x/<number>i addr查看addr开始的内存,并以指令格式显示,显示指令的条数为<number>,x/i addr表示将addr开始的内存examine as instructions,且指令条数为1。
(gdb) x/10i 0xfe05b 0xfe05b: cmpl $0x0,%cs:0x6ac8 0xfe062: jne 0xfd2e1 0xfe066: xor %dx,%dx 0xfe068: mov %dx,%ss 0xfe06a: mov $0x7000,%esp 0xfe070: mov $0xf34c2,%edx 0xfe076: jmp 0xfd15c 0xfe079: push %ebp 0xfe07b: push %edi 0xfe07d: push %esi (gdb)
第一条指令为cmp指令,立即数0x0与内存单元CS:0x6ac8中的数据比较大小。相等则将flags中的ZF位(Zero Flag)置为1;不等则置为0。第二条指令为jne指令(Jump if Not Equal),若ZF=0则跳转至标号处执行。使用examine(简写为x)来查看内存中的内容。
(gdb) x 0xf6ac8 0xf6ac8: 0 (gdb)
所以不会跳转。可以看出jne后的3条指令为设置栈段与栈顶的指令。继续执行一直到0xfe076之后,跳转到0xfd15c。
(gdb) si [f000:d15c] 0xfd15c: mov %eax,%ecx 0x0000d15c in ?? () (gdb)
查看0xfd15c开始的10条指令。
(gdb) x/10i 0xfd15c 0xfd15c: mov %eax,%ecx 0xfd15f: cli 0xfd160: cld 0xfd161: mov $0x8f,%eax 0xfd167: out %al,$0x70 0xfd169: in $0x71,%al 0xfd16b: in $0x92,%al 0xfd16d: or $0x2,%al 0xfd16f: out %al,$0x92 0xfd171: lidtw %cs:0x6ab8 (gdb)
- cli:禁止中断发生
- cld:将标志寄存器Flag的方向标志位DF清零
- out与in:通过Ports(70、71、92)与I/O设备交互
- or:按位逻辑或
继续查看
(gdb) x/10i 0xfd171 0xfd171: lidtw %cs:0x6ab8 0xfd177: lgdtw %cs:0x6a74 0xfd17d: mov %cr0,%eax 0xfd180: or $0x1,%eax 0xfd184: mov %eax,%cr0 0xfd187: ljmpl $0x8,$0xfd18f 0xfd18f: mov $0x10,%ax 0xfd192: add %al,(%bx,%si) 0xfd194: mov %ax,%ds 0xfd196: mov %ax,%es (gdb)
- cr0:control register,which changes or controls the general behavior of a CPU or other digital device
| Bit | Name | Full Name | Description |
|---|---|---|---|
| 0 | PE | Protected Mode Enable | If 1, system is in protected mode, else system is in real mode |
- ljmp:ljmp specifies a code segment to switch to in addition to the address to jump to
查看0xfd18f开始的10条指令(此时工作在保护模式中)
(gdb) x/10i 0xfd18f 0xfd18f: mov $0x10,%eax 0xfd194: mov %eax,%ds 0xfd196: mov %eax,%es 0xfd198: mov %eax,%ss 0xfd19a: mov %eax,%fs 0xfd19c: mov %eax,%gs 0xfd19e: mov %ecx,%eax 0xfd1a0: jmp *%edx 0xfd1a2: mov %eax,%ecx 0xfd1a4: mov $0x20,%eax (gdb)
可以看出主要是对寄存器赋值。
The Boot Loader
Exercise 3. Take a look at the lab tools guide, especially the section on GDB commands. Even if you're familiar with GDB, this includes some esoteric GDB commands that are useful for OS work.
Set a breakpoint at address 0x7c00, which is where the boot sector will be loaded. Continue execution until that breakpoint. Trace through the code in boot/boot.S, using the source code and the disassembly file obj/boot/boot.asm to keep track of where you are. Also use the x/i command in GDB to disassemble sequences of instructions in the boot loader, and compare the original boot loader source code with both the disassembly in obj/boot/boot.asm and GDB.
Trace into bootmain() in boot/main.c, and then into readsect(). Identify the exact assembly instructions that correspond to each of the statements in readsect(). Trace through the rest of readsect() and back out into bootmain(), and identify the begin and end of the for loop that reads the remaining sectors of the kernel from the disk. Find out what code will run when the loop is finished, set a breakpoint there, and continue to that breakpoint. Then step through the remainder of the boot loader.
At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?
从boot.S中以下代码开始:
# Switch from real to protected mode, using a bootstrap GDT # and segment translation that makes virtual addresses # identical to their physical addresses, so that the # effective memory map does not change during the switch. lgdt gdtdesc movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0 # Jump to next instruction, but in 32-bit code segment. # Switches processor into 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?
Boot loader调用的最后一条指令是位于main.c中的
// call the entry point from the ELF header // note: does not return! ((void (*)(void)) (ELFHDR->e_entry))();
在boot.asm中得知该指令地址为0x7d6b,打断点执行至该指令处。下一条指令为
=> 0x7d6b: call *0x10018
表示跳转至[10018]处,查看内存
(gdb) x/1xw 0x10018 0x10018: 0x0010000c
则下一条指令(即kernel的第一条指令位于0x1000c),查看内存
(gdb) x/i 0x10000c => 0x10000c: movw $0x1234,0x472
此即为kernel的第一条指令。
Where is the first instruction of the kernel?
由前可知,kernel的第一条指令位于0x1000c。
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?
在main.c中
// load each program segment (ignores ph flags) 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);
显然,是根据Boot loader读入的第一个sector(ELFHDR)的e_phnum决定的。
Loading the kernel
Exercise 4. Read about programming with pointers in C. The best reference for the C language is The C Programming Language by Brian Kernighan and Dennis Ritchie (known as 'K&R'). We recommend that students purchase this book (here is an Amazon Link) or find one of MIT's 7 copies.
Read 5.1 (Pointers and Addresses) through 5.5 (Character Pointers and Functions) in K&R. Then download the code for pointers.c, run it, and make sure you understand where all of the printed values come from. In particular, make sure you understand where the pointer addresses in printed lines 1 and 6 come from, how all the values in printed lines 2 through 4 get there, and why the values printed in line 5 are seemingly corrupted.
There are other references on pointers in C (e.g., A tutorial by Ted Jensen that cites K&R heavily), though not as strongly recommended.
Warning: Unless you are already thoroughly versed in C, do not skip or even skim this reading exercise. If you do not really understand pointers in C, you will suffer untold pain and misery in subsequent labs, and then eventually come to understand them the hard way. Trust us; you don't want to find out what "the hard way" is.
比较基本的指针知识。
To make sense out of boot/main.c you'll need to know what an ELF binary is. When you compile and link a C program such as the JOS kernel, the compiler transforms each C source ('.c') file into an object ('.o') file containing assembly language instructions encoded in the binary format expected by the hardware. The linker then combines all of the compiled object files into a single binary image such as obj/kern/kernel, which in this case is a binary in the ELF format, which stands for "Executable and Linkable Format".
An ELF binary starts with a fixed-length ELF header, followed by a variable-length program header listing each of the program sections to be loaded. The C definitions for these ELF headers are in inc/elf.h. The program sections we're interested in are:
- .text: The program's executable instructions.
- .rodata: Read-only data, such as ASCII string constants produced by the C compiler. (We will not bother setting up the hardware to prohibit writing, however.)
- .data: The data section holds the program's initialized data, such as global variables declared with initializers like int x = 5;.
When the linker computes the memory layout of a program, it reserves space for uninitialized global variables, such as int x;, in a section called .bss that immediately follows .data in memory. C requires that "uninitialized" global variables start with a value of zero. Thus there is no need to store contents for .bss in the ELF binary; instead, the linker records just the address and size of the .bss section. The loader or the program itself must arrange to zero the .bss section.
Examine the full list of the names, sizes, and link addresses of all the sections in the kernel executable by typing:
objdump -h obj/kern/kernel
shawn@ubuntu:~/6.828/lab$ objdump -h obj/kern/kernel obj/kern/kernel: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 000019e9 f0100000 00100000 00001000 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .rodata 000006c0 f0101a00 00101a00 00002a00 2**5 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .stab 00003b95 f01020c0 001020c0 000030c0 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .stabstr 00001948 f0105c55 00105c55 00006c55 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .data 00009300 f0108000 00108000 00009000 2**12 CONTENTS, ALLOC, LOAD, DATA 5 .got 00000008 f0111300 00111300 00012300 2**2 CONTENTS, ALLOC, LOAD, DATA 6 .got.plt 0000000c f0111308 00111308 00012308 2**2 CONTENTS, ALLOC, LOAD, DATA 7 .data.rel.local 00001000 f0112000 00112000 00013000 2**12 CONTENTS, ALLOC, LOAD, DATA 8 .data.rel.ro.local 00000044 f0113000 00113000 00014000 2**2 CONTENTS, ALLOC, LOAD, DATA 9 .bss 00000648 f0113060 00113060 00014060 2**5 CONTENTS, ALLOC, LOAD, DATA 10 .comment 00000029 00000000 00000000 000146a8 2**0 CONTENTS, READONLY
Take particular note of the "VMA" (or link address) and the "LMA" (or load address) of the .text section. The load address of a section is the memory address at which that section should be loaded into memory.
The link address of a section is the memory address from which the section expects to execute. The linker encodes the link address in the binary in various ways, such as when the code needs the address of a global variable, with the result that a binary usually won't work if it is executing from an address that it is not linked for. (It is possible to generate position-independent code that does not contain any such absolute addresses. This is used extensively by modern shared libraries, but it has performance and complexity costs, so we won't be using it in 6.828.)
VMA为Virtual Memory Address,为虚拟内存地址,为Linker链接之后的地址;LMA为Load Memory Address,为加载内存地址,为Loader装入内存之后的地址。
shawn@ubuntu:~/6.828/lab$ objdump -h obj/boot/boot.out obj/boot/boot.out: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00000186 00007c00 00007c00 00000074 2**2 CONTENTS, ALLOC, LOAD, CODE 1 .eh_frame 000000a8 00007d88 00007d88 000001fc 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .stab 0000087c 00000000 00000000 000002a4 2**2 CONTENTS, READONLY, DEBUGGING 3 .stabstr 00000925 00000000 00000000 00000b20 2**0 CONTENTS, READONLY, DEBUGGING 4 .comment 00000029 00000000 00000000 00001445 2**0 CONTENTS, READONLY
可以看出boot.out中VMA与LMA相等。
Back in boot/main.c, the ph->p_pa field of each program header contains the segment's destination physical address.
Exercise 5. 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, run make clean, recompile the lab with make, and trace into the boot loader again to see what happens. Don't forget to change the link address back and make clean again afterward!
Look back at the load and link addresses for the kernel. Unlike the boot loader, these two addresses aren't the same: the kernel is telling the boot loader to load it into memory at a low address (1 megabyte), but it expects to execute from a high address. We'll dig in to how we make this work in the next section.
Besides the section information, there is one more field in the ELF header that is important to us, named e_entry. This field holds the link address of the entry point in the program: the memory address in the program's text section at which the program should begin executing. You can see the entry point:
shawn@ubuntu:~/6.828/lab$ 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
可以看出与之前的分析相符合。
You should now be able to understand the minimal ELF loader in boot/main.c. It reads each section of the kernel from disk into memory at the section's load address and then jumps to the kernel's entry point.
Exercise 6. We can examine memory using GDB's x command. The GDB manual has full details, but for now, it is enough to know that the command x/Nx ADDR prints N words of memory at ADDR. (Note that both 'x's in the command are lowercase.) Warning: The size of a word is not a universal standard. In GNU assembly, a word is two bytes (the 'w' in xorw, which stands for word, means 2 bytes).
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.)
(gdb) b *0x7c00 Breakpoint 1 at 0x7c00 (gdb) b *0x10000c Breakpoint 2 at 0x10000c (gdb) c Continuing. [ 0:7c00] => 0x7c00: cli Breakpoint 1, 0x00007c00 in ?? () (gdb) x/8x 0x100000 0x100000: 0x00000000 0x00000000 0x00000000 0x00000000 0x100010: 0x00000000 0x00000000 0x00000000 0x00000000 (gdb) c Continuing. The target architecture is assumed to be i386 => 0x10000c: movw $0x1234,0x472 Breakpoint 2, 0x0010000c in ?? () (gdb) x/8x 0x100000 0x100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766 0x100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8
之前查看了kernel中各个section的装入地址,可以看出.text被装入至0x100000处。
The Kernel
Operating system kernels often like to be linked and run at very high virtual address, such as 0xf0100000, in order to leave the lower part of the processor's virtual address space for user programs to use.
Many machines don't have any physical memory at address 0xf0100000, so we can't count on being able to store the kernel there. Instead, we will use the processor's memory management hardware to map virtual address 0xf0100000 (the link address at which the kernel code expects to run) to physical address 0x00100000 (where the boot loader loaded the kernel into physical memory).
Exercise 7. Use QEMU and GDB to trace into the JOS kernel and stop at the movl %eax, %cr0. Examine memory at 0x00100000 and at 0xf0100000. Now, single step over that instruction using the stepi GDB command. Again, examine memory at 0x00100000 and at 0xf0100000. Make sure you understand what just happened.
What is the first instruction after the new mapping is established that would fail to work properly if the mapping weren't in place? Comment out the movl %eax, %cr0 in kern/entry.S, trace into it, and see if you were right.
观察obj/kern/kernel.asm可知,movl %eax, %cr0的link address为0xf0100025,load address为0x100025。打断点并执行至该处
(gdb) b *0x100025 Breakpoint 1 at 0x100025 (gdb) c Continuing. The target architecture is assumed to be i386 => 0x100025: mov %eax,%cr0 Breakpoint 1, 0x00100025 in ?? () (gdb) x/8x 0x100000 0x100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766 0x100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8 (gdb) x/8x 0xf0100000 0xf0100000 <_start+4026531828>: 0x00000000 0x00000000 0x00000000 0x00000000 0xf0100010 <entry+4>: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) si => 0x100028: mov $0xf010002f,%eax 0x00100028 in ?? () (gdb) x/8x 0x100000 0x100000: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766 0x100010: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8 (gdb) x/8x 0xf0100000 0xf0100000 <_start+4026531828>: 0x1badb002 0x00000000 0xe4524ffe 0x7205c766 0xf0100010 <entry+4>: 0x34000004 0x2000b812 0x220f0011 0xc0200fd8 (gdb)
可以看出,执行movl %eax, %cr0命令之后,高地址空间已被映射至低地址空间。将该命令注释后重新运行。
# Turn on paging. movl %cr0, %eax f010001d: 0f 20 c0 mov %cr0,%eax orl $(CR0_PE|CR0_PG|CR0_WP), %eax f0100020: 0d 01 00 01 80 or $0x80010001,%eax # movl %eax, %cr0 # Now paging is enabled, but we're still running at a low EIP # (why is this okay?). Jump up above KERNBASE before entering # C code. mov $relocated, %eax f0100025: b8 2c 00 10 f0 mov $0xf010002c,%eax jmp *%eax f010002a: ff e0 jmp *%eax f010002c <relocated>: relocated: # Clear the frame pointer register (EBP) # so that once we get into debugging C code, # stack backtraces will be terminated properly. movl $0x0,%ebp # nuke frame pointer f010002c: bd 00 00 00 00 mov $0x0,%ebp
why is this okay?是因为该段程序之前已被Boot Loader装入内存。重新调试
(gdb) si => 0xf010002c <relocated>: add %al,(%eax) relocated () at kern/entry.S:74 74 movl $0x0,%ebp # nuke frame pointer (gdb) si Remote connection closed
kernel报错信息
qemu: fatal: Trying to execute code outside RAM or ROM at 0xf010002c
可以看出是在0xf010002c处出错,因为0xf010002a处(已被Boot Loader装入内存)的指令要求跳转至0xf010002c。由于未开启paging,无法将0xf010002c映射至低地址,故出错。
Formatted Printing to the Console
Read through kern/printf.c, lib/printfmt.c, and kern/console.c, and make sure you understand their relationship. It will become clear in later labs why printfmt.c is located in the separate lib directory.
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.
这段代码在lib/printfmt.c中Line206 - 212
// (unsigned) octal case 'o': // Replace this with your code. putch('X', putdat); putch('X', putdat); putch('X', putdat); break;
仿照%u,可以较容易地写出
// (unsigned) octal case 'o': // Replace this with your code. num = getuint(&ap, lflag); base = 8; goto number;
Be able to answer the following questions:
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?
console.c中调用了printf.c中定义的putch()方法。而printf.c中的putch()方法则是包装了console.c中的cputchar()方法。
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)满时,刷新一行,并将最下面一行置为白底空格。
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);
- In the call to
cprintf(), to what doesfmtpoint? To what doesappoint?- List (in order of execution) each call to
cons_putc,va_arg, andvcprintf. Forcons_putc, list its argument as well. Forva_arg, list whatappoints to before and after the call. Forvcprintflist the values of its two arguments.
4. Run the following code.
unsigned int i = 0x00646c72; cprintf("H%x Wo%s", 57616, &i);What is the output? Explain how this output is arrived at in the step-by-step manner of the previous exercise. Here's an ASCII table that maps bytes to characters.The output depends on that fact that the x86 is little-endian. If the x86 were instead big-endian what would you set
ito in order to yield the same output? Would you need to change57616to a different value?Here's a description of little- and big-endian and a more whimsical description.
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?cprintf("x=%d y=%d", 3);
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
cprintfor its interface so that it would still be possible to pass it a variable number of arguments?
Challenge Enhance the console to allow text to be printed in different colors. The traditional way to do this is to make it interpret ANSI escape sequences embedded in the text strings printed to the console, but you may use any mechanism you like. There is plenty of information on the 6.828 reference page and elsewhere on the web on programming the VGA display hardware. If you're feeling really adventurous, you could try switching the VGA hardware into a graphics mode and making the console draw text onto the graphical frame buffer.
The Stack
浙公网安备 33010602011771号