实验笔记
AbstractMachine¶
- 一个抽象层,为 c 语言提供运行环境,简化了系统的开发方式,使得可以用编写 c 语言的方式来编写操作系统
- AbstractMachine 解决的问题是 “能否在不理解硬件机制细节的前提下实现操作系统”。
- 提供 5 组 (15 个) 主要 API,可以实现各类系统软件 (如操作系统):
- (TRM)
putch
/halt
- 最基础的计算、显示和停机 - (IOE)
ioe_read/ioe_write
- I/O 设备管理 - (CTE)
ienabled
/iset
/yield
/kcontext
- 中断和异常 - (VME)
protect
/unprotect
/map
/ucontext
- 虚存管理 - (MPE)
cpu_count
/cpu_current
/atomic_xchg
- 多处理器
- (TRM)
什么是裸机 bare-metal 编程¶
// say.c
void putch(char ch);
int putchar(int ch);
void say(const char *s) {
for (; *s; s++) {
#ifdef __ARCH__
putch(*s); // AbstractMachine,没有 libc,调用 TRM API 打印字符
#else
putchar(*s); // 操作系统,调用 libc 打印字符
#endif
}
}
c 程序如何运行起来¶
- 在操作系统上
- 在程序连接时 gcc 替我们完成了很多事情
gcc main.o say.o
$ ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \ /usr/lib/x86_64-linux-gnu/crt1.o \ /usr/lib/x86_64-linux-gnu/crti.o \ main.o say.o -lc \ /usr/lib/x86_64-linux-gnu/crtn.o
ld-linux-x86-64.so
负责动态链接库的加载;-crt*.o
是 C Runtime 的缩写,即 C 程序运行所必须的一些环境,例如程序的入口函数_start
(二进制文件并不是从main
开始执行的!)、atexit
注册回调函数的执行等;--lc
表示链接 glibc。链接后得到一个 ELF 格式的可执行文件-
加载与执行:
- Shell 接收到命令后,在操作系统中使用
fork()
创建一个新的进程。 - 在子进程中使用
execve()
加载a.out
。操作系统内核中的加载器识别出a.out
是一个动态链接文件,做出必要的内存映射 - 程序运行过程中,如需进行输入/输出等操作 (如 libc 中的
putchar
),则会使用特殊的指令 (例如 x86 系统上的int
或syscall
) 发出系统调用请求操作系统执行。
- Shell 接收到命令后,在操作系统中使用
-
在 AbstractMachine 上
- 只链接了
main.o
,say.o
和必要的库函数 (AbstractMachine 和 klib;- 创建了
hello-x86_64-qemu
的镜像文件,镜像文件是由 512 字节的 “MBR”、1024 字节的空白 (用于存放main
函数的参数) 和hello-x86_64-qemu.o
组成的。用file
类型可以识别出它:
- 创建了
- 生成后的程序不能直接在操作系统上运行,需要在 bare-metal 上加载它
- 加载与运行
- 在 QEMU 全系统模拟器中运行完整的镜像
$ qemu-system-x86_64 -S -s -serial none -nographic hello-x86_64-qemu
-S
在模拟器初始化完成 (CPU Reset) 后暂停-s
启动 gdb 调试服务器,可以使用 gdb 调试模拟器中的程序-serial none
忽略串口输入/输出-nographics
不启动图形界面-serial stdio
将串口的输入输出重定向到终端(不然在终端是看不到程序中 putchar 的输出的)
- 安装
sudo apt install qemu-kvm qemu virt-manager virt-viewer libvirt-daemon-system libvirt-clients bridge-utils
- 和操作系统上的 C 程序不同,AbstractMachine 上的程序对计算机硬件系统有完整的控制, 如用 I/O 指令和 memory-mapped I/O 直接和物理设备交互
- 在 QEMU 全系统模拟器中运行完整的镜像
规约(具体的方法)¶
IOE¶
bool ioe_init();
完成系统中 I/O 设备的初始化 (一次 kernel 运行中只能调用一次,且必须在mpe_init
之前)void ioe_read (int reg, void *buf);/void ioe_write(int reg, void *buf);
设备读写- 从编号为
reg
的寄存器读取/写入,读取/写入的数据取决于寄存器的编号。
- 从编号为
- 对于多处理器不是安全的,对同一个 IO 设备的访问必须互斥(任意时刻对同一个设备只允许有一个尚未返回的
ioe_read
或ioe_write
操作) - klib-macros.h 中的封装宏定义
#define io_read(reg) \ ({ reg##_T __io_param; \ ioe_read(reg, &__io_param); \ __io_param; }) #define io_write(reg, ...) \ ({ reg##_T __io_param = (reg##_T) { __VA_ARGS__ }; \ ioe_write(reg, &__io_param); })
封装库函数 klib¶
- klib:自定义的 glib 库,用于打包常用 API,如用 printf 打包系统的 putch,要注意多线程安全等问题,使用 assertion 多加检查
32/64 兼容问题¶
数据类型兼容性¶
intptr_t
与uintptr_t
- 这两种类型的主要用途是在需要将指针存储为整数或将整数转换为指针时,确保类型的大小足以存储指针值,从而保证代码在不同平台(32位和64位)上的可移植性和安全性。
- 需要使用的情况:指针与整数转化;指针的算术运算;
- 不需要的情况:使用标准库函数(如 malloc),纯粹的指针的操作,不涉及和整数类型的转换