2022/11
unixv6
xv6
(2022/11/20) 發現這本難得的好書, 對 Unix/Linux kernel 的實作了解大有幫助. 該作者目前已經發行四本(實體)書, 分別是
For English documentation related to xv6
, I found two articles valueable, they are official xv6 book (pdf) - xv6 a simple, Unix-like teaching operating system, and Lions' Commentary on UNIX' 6th Edition, John Lions (pdf version)
latest update on 2023/02/27
Table of Contents
本文主要參考 2 本書的心得 "操作系統原型 - xv6 分析與實踐 羅秋明 著" 跟 "xv6 a simple, Unix-like teaching operating system".
喜歡實作的人, 可以從 "Chapter 1 xv6 installation" 開始操作, 下載 xv6 原始碼, 進行編譯及修改, 再進入 xv6 的原理, 介紹 bootstrap 到 kernel 的實踐.
如果是喜歡先理解原理的人, 可以先從 xv6 a simple Unix like teaching operating system 的 Appendix A & B 開始讀起.
A : xv6 是個教學用的操作系統, 是 Unix Version 6 (v6) 的簡單實現, 但是並不嚴格遵守 v6 的結構與風格. 2020/8/11 後已不再維護, 而轉向 RISC-V 版本.
xv6 is a re-implementation of Dennis Ritchie's and Ken Thompson's Unix Version 6 (v6). xv6 loosely follows the structure and style of v6,
but is implemented for a modern x86-based multiprocessor using ANSI C.
xv6 is inspired by John Lions's Commentary on UNIX 6th Edition (Peer
to Peer Communications; ISBN: 1-57398-013-7; 1st edition (June 14,
2000)). See also https://pdos.csail.mit.edu/6.828/, which
provides pointers to on-line resources for v6.
xv6 borrows code from the following sources:
從 https://github.com/mit-pdos/xv6-public/tags 下載 rev9 版本 xv6-rev9.tar.gz 後就可以在 QEMU 上執行, 或在 QEMU / gdb 執行.
In above 3.2-1 example, we will find below message after launching gdb
, and learn that there is a .gdbinit
file in xv6 directory which set up the environment for gdb
client. However, gdb
does not execute that .gdinit
for security concern. We need to enable it manually. In 3.2-2, we specify the working directory of xv6 and .gdbinit
in command line option gdb -iex
. Or in 3.2-3, we create a file .gdbinit
under ~/
directory with the content of "set auto-load safe-path /home/kernel-dev/myworks/xv6-public"
so gdb
will execute xv6 .gdbinit
to set up environments. Either way can work.
For those who are not familiar with QEMU and gdb, you might need to know the commands how to exit. Without knowing those commands, you will be annoyed by how to exit QEMU and gdb.
qemu-nox
command)qemu
command)Please look for more QEMU and gdb commands in respective manuals.
After launching xv6 CPUS=4 make qemu-nox
, it shows shell command prompt $
. (One strange thing is, xv6 system becomes very slow when I set CPUS to 8 or 16. Could be an interesting topic to dive into details)
After entering xv6 system, we can issue ls
command to check what else commands available, as in left column of below table. Also list the files in xv6 source code with *.c
as reference. There are quite some differences beween both, as the xv6
column shows the commands in user space, and host Linux
shows commands/API's for both user and kernel space.
host Linux | qemu - xv6 |
People familiar with Linux know the command ps
to list the active processes running (in foreground and background). In xv6, there is a special command, or should be called key strokes, of CTRL+p, to print out the active processes. Below is the example when CTRL+p were pressed at vx6 prompt.
Then we switch back to the host Linux environment, to check what those numbers mean in xv6 source code.
This section can be skipped if you are not (yet) intestered in using Makefile
to build xv6 (or other Linux) image. There are 2 steps to generate the xv6 disk file system.
Makefile
snippet to build all applicaions.Each %.o
file will link $(LD)
with $(ULIB)
to produce its executable file named _%
. (%
will be replaced by cat
, echo
, or other user applications)
-Ttext 0
to assign the code to start from address 0
.
-e main
indicates to use main
function as the first instruction.
-N
is to specify the data
and text
sections allows read
and write
, and no need to align with page boundary.
UPROGS
parameter includes all the related executable file names. Then generate fs.img
by mkfs
with README and UPROGS
files.Find the file main.c
under xv6
source code, find below code snippet.
Modify the cprintf
line by adding xv6
. Then run make qemu-nox
to see the greeting has changed with your modification.
To add one shell command/application in user space of xv6 requires two actions:
my-app.c
(or other file name you prefer), under the directory of xv6-public
, same with Makefile
and other applications.Makefile
, add _my-app
(or other file name you pick) to the parameter UPROGS
.my-app.c
file content
Add one line of _my-app
to the UPROGS
parameter in Makefile
.
Let's check the result.
host Linux | qemu - xv6 |
Unlike adding an application in user space, it take much more procedures to add a system call in kernel space (all applications are in user space, not in kernel space. In kernel space, it is called system call).
To create a system call in kernel, it requires the following steps:
syscall.h
user.h
usys.S
syscall.c
sysproc.c
proc.c
defs.h
pcpuid.c
Makefile
make qemu
Now, execute make qemu
to run into xv6 shell
, and run pcpuid
. (with CPUS=4 make qemu
might get different pcpuid
results.)
You make it
The book explains in a little bit more details on how those files work.
syscall.h
: In xv6, each System Call has one unique ID, so adding SYS_getcpuid
with ID of 22
.user.h
: Declare user state entry function getcpuid
to the header file of user.h
- int getcpuid(void);
. Then it can be called by user application program.usys.S
: usys.S
defines a macro SYSCALL(getcpuid)
to move SYS_getcpuid=22
to register eax
, then issue interrupt command int $T_SYSCALL
Add one line in usys.S
syscall.c
: Define the entry for syscall sys_getcpuid()
when issueing int $T_SYSCALL
and eax
= 22.Below is the syscall()
snippet
Until now, the set up is ready for both getcpuid()
and sys_getcpuid()
. We are going to implement the code for both. sys_getcpuid()
is defined in the source file sysproc.c
, and getcpuid()
defined in proc.c
.
5. Modify sysproc.c
: Implement sys_getcpuid()
function. It just calls getcpuid()
directly.
proc.c
: Implement getcpuid()
in proc.c
. It is quite straight forward to call cpuid()
. However, it needs to disable interrupt first before calling it, so cli()
is added before and sti()
after.defs.h
: defs.h
defines (almost) all the xv6
kernal data structure and functions. In order to get sys_getcpuid()
within sysproc.c
to be able to call getcpuid()
, we need to add int getcpuid(void);
in defs.h
.pcpuid.c
: Relatively easy as other regular user applications.gdb
Now I understand better the design, but still not clear exactly how the program will flow among all those functional calls. So use gbd
to trace the process flow.
qemu
terminal screen
gdb
terminal screen
Back to qemu
screen
We will see gdb
screen pops up with Breakpint like below
Based on above gdb
findings and previous coding, I try to imagine the process flow like below. Though it needs to be validated. Advise is welcome, and more works to do
getpid
Try to understand the other (simple) System Call - getpid
, to see if getpid
shows up at exactly the same source files.
getpid
only shows up in above 1,2,4,5 files (syscall.h
, user.h
, syscall.c
, sysproc.c
) modified for gcpuid
, plus user application usertests.c
, but not in usys.S
, proc.c
, defs.h
.
xv6 binary consists of 2 sections :
bootblock
binutils
tool. For those (including me) who are not familiar with ELF format, bootloader and binutils
tool can refer to Chap 4 of another book <<Linux GNU C 程式观察>> by the same author.Simplify the boot loader by putting kernel on the same disk image with boot loader
Reference from appendix B of xv6 a simple, Unix-like teaching operating system, or translation in Mandarin xv6代码阅读:系统引导. There is a draft version from Cox, Kaashoek, Morris released in 2010, which explains in more details about bootstrap than later version in 2012.
The boot loader compiles to around 470 byes (definitely needs to be less than 510bytes, to fit into one sector, plus magic words of 0x55aa.) of machine code, depending on the optimizations used when compiling C code. In order to fit in that small amout of space, the xv6 boot loader makes a major simpifying assumption, that the kernel has been written to the boot disk contiguously starting at sector 1. (Boot loader was stored at sector 0 of the boot disk). We know that from the Makefile
of xv6
. It is unlike modern PC uses a two-step boot process.
So xv6
boot loader relies on the less space constraint BIOS for disk access rather than trying to drive the disk itself.
bootblock
descriptionTo understand how PC works from scratch, it might require some computer architecture and hardware/firmware/software, and some PC development history. Can refer to articles MBR 載入位址 0x7C00 的來源與意義的調查結果, or From the bootloader to the kernel.
x86 system BIOS (after checking hardware, or called POST, Power On Self Test) or qemu
will load the first 512 byts boot code (or MBR from HDD) to RAM memory address 0x7c00, and jump to 0x7c00 to execute the boot code, it is bootblock
used in xv6
. This 512 byte boot code will further load the kernel from HDD, then transfer the control to kernel.
bootblock
file size is 512 bytes, and MBR boot sector
when checked by file
instruction.
bootblock
is generated, with starting address from $7c00A : Appendix B - The boot loader of "xv6 a simple, Unix-like teaching operating system" provides a clear view on boot sequence. It explains how bootasm.S
and bootmain.c
works.
Below chart is referred from the book "操作系統原型 - xv6 分析與實踐 羅秋明 著".
1. About which one is first between bootasm.S
and bootmain.c
? : Even I know the BIOS entry is $7c00, but I was not sure which one will bootblock
start first? bootasm.S
or bootmain.c
. (Normally, I would expect the program shall start from main(), haha, which is not in this case.) Finally, I learned from Compiling & Linking that the linker scans the relocatable obj files and archives left to right in the same sequential order that they appear on the compiler driver's command line. (The driver automatically translates any .c files on the cmd line into .o files.) And in xv6 Makefile
, the bootasm.o
is in front of bootmain.o
. So when BIOS completes the hardware setup, load the 'bootloader' 512 bytes into the memory address of $7c00, xv6
will start from bootasm.S
.
Also, we can cross check with the first few line of bootblock.asm
, which is exactly the same as the beginning of bootasm.S
.
2. bootasm.S
Code - Assembly bootstrap :
Reference from appendix B of xv6 a simple, Unix-like teaching operating system, or translation in Mandarin xv6代码阅读:系统引导
The main functions of xv6 bootasm.S is to initiates the CPU and the system
2.1 Clear interrup - cli
2.2 Zero data segment registers DS, ES and SS
2.3 Enable A20 line to enable address capability beyond 1MB. (More info from A20 - a pain from the past)
2.4 Switch from real to protected mode by loading GDT, and enable CR0_PE bit
2.5 Complete the transition to 32-bit protected mode to reload %cs and %eip by using a long jmp instruction
2.6 Tell assembler to generate 32 bit code from now on. Then, set up the protected-mode data segment registers. Select Data Segement for DS, ES, and SS, null Segment for FS, and GS.
2.7 Set up Stack Pointer and call into bootmain.c
2.8 If bootmain.c
returns (it shouldn't but it would happen with error), trigger a Boch kind of emulator, then hangs by an infinite loop.
2.1 bootasm.S
Code - 80286/80386 protected mode
and legacy 8086 real mode
Before diving into how bootasm.S
works, let's understand how it is defined in the Segment Descriptor, and some lagecy from 8086 to 80286, then 80386, and lately 64 bit. xv6 (and Linux) starts from 80386 design, however, x86 CPU always boots up in 8086 real mode first, so is the reason to under the lagacy.
One thing worthwhile noticing is mentioned earlier that bootmain()
, the xv6 boot loader, makes a major simpifying assumption, that the kernel has been written to the boot disk contiguously starting at sector 1. And it uses BIOS API to read the kernel images into memory.
This article - 2.1. Internal Microprocessor Architecture describes the evolution from 8086 real mode
to 80286 (16bit)/80386(32bit) protected mode
.
Figure below shows the format of a descriptor for the 80286 through the Pentium II. Note that each descriptor is 8 bytes in length, so the global and local descriptor tables are each a maximum of 64K bytes in length. Descriptors for the 80286 and the 80386 through the Pentium II differ slightly, but the 80286 descriptor is upward-compatible (with reserved 2 bytes). Though we can see the 'ugly' structure of descriptor in 80386 to be backward compatible with 80286.
2.2 bootasm.S
Code - Implementation of Global Descriptor Table (GDT)
Segment Descriptor entry has a complex structure. (Reference from osdev - GDT)
Below are code snippet related to GDP in xv6/x86.h
and bootasm.S
.
xv6/x86.h
xv6/bootasm.S
xv6
code lines 0660-0663
SEG_ASM(type,base,lim)
does the job of converting the values of type
, base
and limit
to the structure required by the Segment Descriptor.
line 8484
sets up for code segment, and line 8485 sets up for data segment.
So, the Segment Descript of code segment
in xv6 bootloader
looks like this
And data segment
looks like this
3. bootmain.c
Code - C bootstrap:
As described in bootmain.c
, bootmain()
loads an ELF kernel image from the disk starting at sector 1 and then jumps to the kernel entry routine. That is the only task of what bootmain.c
does.
3.1 bootmain.c
C bootstrap memory map:
xv6
Memory map with reference from xv6代码阅读:系统引导.
3.1.1 Memory map loading 512 bytes from disc sector 0
The CPU is in real
mode, in which it simulates an Intel 8088. In real mode, there are eight 16-bit general registers, but the processor sends 20 bits of address to memory. The segment registers %cs, %ds, %es, and %ss provide the additional bits necessary to generate 20-bit memory address from 16-bit registers. That gives x86 CPU with addressing capability of maximum 1MBytes
in real mode. We will call the segment:offset as virtual memory reference in real mode, and processor chip sends to memory 20-bit physical addresses.
Quote from Cox, Kaashoek, Morris - Bootstrap
Real mode’s 16-bit general-purpose and segment registers make it awkward for a program to use more than 65,536 bytes of memory, and impossible to use more than a megabyte. Modern x86 processors have a "protected mode" which allows physical addresses to have many more bits, and a "32-bit" mode that causes registers, virtual addresses, and most integer arithmetic to be carried out with 32 bits rather than 16. The xv6 boot sequence enables both modes.
(x86 default) BIOS loads the first 512 bytes from disk, load it to 0x7C00 memory address, and jump to 0x7C00 to start boot sequence.
**3.1.2 Memory map after bootasm.S
and bootmain.c
**
bootasm.S
Check x86 inline assembly for detail explanation on inline aseembly in bootmain.c
line 8573 insl(0x1F0, dst, SECTSIZE/4);
A : 簡介 file descriptor (檔案描述符)
fd Number | Name | Function |
---|---|---|
0 | stdin | 標準輸入 |
1 | stdout | 標準輸出 |
2 | stderr | 標準錯誤 |
fork
Page 10, section 1.2.3
Page 23, section 2.3
To those who are interested in boot sequences of xv6, suggest to read below "Appendix A - Source code boot sequence" and "Appendix B - xv6 Bootstrap", from which draft version from Cox, Kaashoek, Morris released in 2010 explains in more details of boot sequence, before reading this book xv6 book (pdf) - xv6 a simple, Unix-like teaching operating system. It also contains this topic in its "Appendix B The boot loader", though with less details.
Item | Source code / Function Entry |
Line no. [1] | Description | Memory address/ Entry point |
Call from | x86 mode | user/kernel mode |
---|---|---|---|---|---|---|---|
1. | bootasm.S /start: |
9100 9111 |
Start of bootloader sequence. BIOS loads this code from first sector (0) of HDD into memory address 0x7c00 - 0x7e00 and starts execution in real mode with %cs=0 %ip=0x7c00 bootasm.S sets up A20 line, switches from real mode to 80286 protected mode, then to 32bit 80386 protected mode. |
0x7c00 - 0x7e00 (just below 32KB)Entry: 0x7c00 |
BIOS | Real -> Protected (16bit) -> Protected (32bit) | |
2. | bootmain.c /bootmain(void) |
9200 9216 |
Part of Bootblock, along with bootasm.S . The function bootmain() loads ELF image (contains elf header and program header ) from HDD sector 1 (right after the bootloader of HDD sector 0), and store in scratch memory address 0x10000 (64KB) for temporary usage. Follow the contents of elf header / program header , and read again from HDD to the memory address specified. Then jump to and execute kernel entry.S entry: routine |
0x7c00 - 0x7e00 (just below 32KB) Entry: 0x7cxx (following bootasm.S, real address depending on xv6 compiled result) |
bootasm.S |
Protected (32bit) | |
3. | entry.S /_start entry: |
1100 1139 1144 |
The xv6 kernel starts executing in this fie. Entering xv6 on boot processor, turn on page size extention, set up stack pointer, jump to main() |
0x100000 - 0x1063ca & 0x1073e0 - 0x107b7e [2] Entry point : 0x10000c(1MB) [3] |
bootmain.c |
Protected (32bit) | |
4. | main.c /main(void) |
1300 1316 |
Bootstrap processor starts running C code here. Allocate a real stack and switch to it, first doing some setup required for memory allocator to work. Tasks are listed in [4] |
0x100000 - 0x1063ca & 0x1073e0 - 0x107b7e [2] Entry point : 0x10xxxx (following entry.S , real address depending on xv6 compiled result) |
entry.S |
Protected (32bit) |
[1]: Line no. of xv6 rev9 source code
[2]: Check page 6 of draft version from Cox, Kaashoek, Morris released in 2010
[3]: Check xv6启动源码阅读 - csdn
[4]: main()
function does the following tasks
There is a draft version from Cox, Kaashoek, Morris released in 2010, which explains in more details about bootstrap than later version in 2016. It uses xv6 rev4 as reference source code, which is different from xv6 rev9 source code pdf used in xv6 book. (we use the rev9 as the reference throughout this note for consistance)
bootasm.S
Below is the complete list of bootasm.S
source code.
bootmain.c
Check wiki - Executable and Linkable Format (ELF) about File header and Program header for both 32 and 64 bit format, which xv6 uses 32 bit format. xv6 defines in xv6/elf.h
about elfhdr
and proghdr
.
Check Stackoverflow - Casting an address to a function: using "void(*)(void)".
((void (*)(void))
It's casting the expression
(ELFHDR->e_entry)
to be a pointer to a function that takes no arguments and returns nothing.
or another explaination
The tool you want is cdecl, which translates C types into English. In this case, it translates:
(void (*)(void))
into:
cast unknown_name into pointer to function (void) returning void
entry.S
main.c
xv6 Makefile $(LD) -b binary initcode entryother
Stackoverflow article $(LD) -b binary explains xv6
how linker work to generate the kernel, with command line below:
As you can see, it uses -b binary to embed the files initcode and entryother, so the above symbols will be defined during this process.
:arrow_left:Previous article - Q&A for Linux
:arrow_right:Next article - x86 RISC-C Implementation
:arrow_up:back to marconi's blog