print("[]("+"https://gitee.com/eatcosmos/linux_kernel_012/blob/books/notes/" + urllib.parse.quote("/".join(IPython.extract_module_locals()[1]["__vsc_ipynb_file__"].split("/")[-1:]))+")")
[](https://gitee.com/eatcosmos/linux_kernel_012/blob/books/notes/1.3.1%20%E6%89%93%E5%8D%B0%20Hello%20World.ipynb)
1.3 第一个内核程序
学习一门 编程语言往往会从打印Hello World 开始,开发操作系统也同样如此。读者在本节将会看到一个最简单的操作系统雏形,它的唯一作用就是在 屏幕上显示“Hello World!",调用的对象是硬件接口罢了,软件再调用操作系统的软件接口。
其实你也没必要把硬件看成硬件,也可以看出软件,总之就是逻辑。
1.3.1 打印 Hello World
首先创建文件bootsect.S,这是一个基于汇编语言的源文件。系统的引导就是从这个文件开始的, 使用汇编语言与硬件打交道是最方便的。本书选择的汇编编译器是 GNU 的 as, 采用的是AT&T 语法。编辑文件内容,如代码清单1-1所示。
函数的本质是一对行为的几何,指令/函数是对一组行为的抽象。
调用硬件使用汇编是因为汇编更方便调用硬件,使用C语言是因为更方便调用操作系统的软件接口。
CPU的计算单元本质上就是一个函数体,而寄存器就是函数的参数,只不过你调用函数的时候,高级语言的那些形式你都要用汇编显式地写出来。这样看来,所谓的高级语言其实只是形式上的高级,其本质并没有提高抽象层次,只是调用多了就显得像是在抽象了。
%%writefile bootsect.S
BOOTSEG = 0x7c0 # 引导程序的段地址
.code16 # 告诉编译器这是一个16位的代码,16位代码可以访问16位的寄存器,32位代码可以访问32位的寄存器
.text # 告诉编译器这是一个文本段,.text 是汇编语言中的一个段名,表示代码段。
.globl _start # 告诉编译器这是一个全局变量,_start是程序的入口
_start:
jmpl $BOOTSEG, $start2 # 远跳转,设置 CS=0x7C0, IP=start2偏移,此时 CS:IP = 0x7C0:start2_offset
start2:
movw $BOOTSEG, %ax # 为什么把段地址赋值给ax寄存器? 因为ax寄存器是16位的寄存器,可以存储16位的数据
movw %ax, %ds # ds = 0x7C0 设置数据段
movw %ax, %es # es = 0x7C0 设置附加段
movw %ax, %fs # fs = 0x7C0 设置其他段寄存器
movw %ax, %gs # gs = 0x7C0 设置其他段寄存器
movw $msg, %ax # 把msg的地址赋值给ax寄存器,msg是字符串,msg的地址是msg的第一个字符的地址
movw %ax, %bp # 把msg的地址赋值给bp寄存器,bp寄存器是16位的寄存器,可以存储16位的数据
movw $0x01301, %ax # 对应ah值为0x13,表示在teletype模式下显示字符串。同时al值为0x01,表示显示输出方式为字符串中含有显示字符串和显示属性,并且显示后光标位置不变。
movw $0x0c, %bx # 在中断号为0x10 功能号为0x13的情况下,bh寄存器存放页码,bl寄存器存放颜色属性。0x0c表示显示红色。
movw $12, %cx # 字符串长度
movb $0, %dl # 表示输出光标位置,dh表示行号、dl表示列号,表示光标在屏幕的左上角
int $0x10 # 通知显卡刷新内容,**中断号**是0x10,**中断功能号**保存在ah中,BIOS 调用显卡刷新内容,查看ah的值
loop:
jmp loop # 死循环,等待中断
msg:
.ascii "Hello World!" # 表示字符串,.ascii 表示字符串,"Hello World!" 是字符串的内容
.org 510 # .org 表示在当前位置插入一个占位符,占位符的大小为510字节
boot_flag:
.word 0xaa55 # 表示引导程序的结束标志,55 aa 是 BIOS 的引导程序的结束标志
%%bash
# 在控制台编译 bootsect.S 文件,命令如下:
as -o bootsect.o bootsect.S
%%bash
ld -m elf_x86_64 -Ttext 0x0 -s --oformat binary -o linux.img bootsect.o # 解释每个参数:
# -m elf_x86_64:指定目标架构为x86-64
# -Ttext 0x0:设置代码段的起始地址为0x0,0x0是虚拟地址,0x7c00是物理地址
# -s:删除所有符号信息
# --oformat binary:指定输出格式为二进制
# -o linux.img:指定输出文件名为linux.img
ls -l linux.img # 应该是 512 字节
hexdump -C linux.img | head -n 10 # 确保最后两个字节是 55 aa
如果一切顺利,则在当前目录下可以看到 linux.img 文件已创建。将这个文件复制到 Bochs 的 linux011 目录下,并在这个目录下执行 run.bat 文件,则会看到 Bochs 虚拟机运行起来以后在屏幕上打印了红色的“Hello World!”,如图 1-1 所示。
# 安装bochs,运行 /tmp/code/linux_kernel_012/notes/1.2.2 配置运行环境.ipynb 中的代码
%%bash
# cp linux.img /tmp/code/linux_kernel_012/notes/linux011
# cd /tmp/code/linux_kernel_012/notes/linux011
# chmod +x /tmp/code/linux_kernel_012/notes/linux011/run.sh
# /tmp/code/linux_kernel_012/notes/linux011/run.sh
在Windows上启动bochs模拟器加载linux.img
我当前实验用的ubuntu服务器没法显示gui界面,终端不知道怎么弄,不折腾了,还是把 linux.img 下载到本地Windows系统吧。
把 /tmp/code/linux_kernel_012/notes/linux011 下载到 C:\Program Files\Bochs-2.7,然后双击C:\Program Files\Bochs-2.7\linux011\run.bat 文件,就可以看到bochs模拟器启动了。
运行成功,屏幕上打印了红色的“Hello World!”
建议优先使用 Alt-2 然后输入 q 的方式,这是最安全的退出方式。
如果使用 QEMU 运行,则需要将 linux.img 复制到 QEMU 所在的目录(C:\Program Files\qemu)中,然后在QEMU 文件路径中打开 PowerShell 或者cmd,并执行如下命令:
# 启动管理员终端
cd "C:\Program Files\qemu"
.\qemu-system-i386.exe -boot a -fda linux.img
代码解释
计算机加载编译偶的代码后,hello world 放到哪里了,给出计算机加载这个代码后的全部详细流程?
1.BIOS 阶段:
- 计算机上电后,CPU从物理地址 0xFFFF0 开始执行(这是 BIOS 入口点)
- BIOS 进行硬件自检
- BIOS 搜索可引导设备(按照 BIOS 中设置的启动顺序)
- 找到可引导设备后,BIOS 将其第一个扇区(512字节也就是软盘上的512字节,也就是linux.img充当软盘)加载到物理内存 0x7C00 处,此时bios就相当于所谓的硬件操作系统,启动扇区就是启动硬盘了。
2. 引导扇区加载:
- 我们的floppya上的512字节的linux.img代码被加载到物理内存 0x7C00 处
- msg字符串"Hello World!"也被加载到这个区域中
- 具体位置:msg标签位置(相对于 0x7C00 的偏移)
3. 程序执行:
jmpl $BOOTSEG, $start2 ; 远跳转,设置 CS=0x7C0, IP=start2偏移
; 此时 CS:IP = 0x7C0:start2_offset
start2:
movw $BOOTSEG, %ax ; ax = 0x7C0
movw %ax, %ds ; ds = 0x7C0 设置数据段
movw %ax, %es ; es = 0x7C0 设置附加段
movw %ax, %fs ; fs = 0x7C0
movw %ax, %gs ; gs = 0x7C0 设置其他段寄存器
movw $msg, %ax ; ax = msg的偏移地址
movw %ax, %bp ; bp = msg的偏移地址(字符串地址)
4.显示字符串:
- int 0x10 是 BIOS 视频服务中断
- AH=13h: 显示字符串功能
- AL=01h: 更新光标位置
- BH=00h: 页号
- BL=0Ch: 红色属性
- CX=12: 字符串长度
- DL=0: 列号
- DH=0: 行号
- ES:BP: 字符串地址 (0x7C0:msg_offset)
5.内存布局
0x00000000 - 0x000003FF: **中断向量表**
0x00000400 - 0x000004FF: **BIOS 数据区**
...
0x0000**7C00** - 0x00007DFF: **引导扇区(我们的代码)**
- 代码
- "Hello World!" 字符串
- 0xAA55 标志(最后两个字节)
...
0x000A0000 - 0x000BFFFF: 显存区域
0x00007C00 是物理内存的出事位置吗?中断表和bios数据区分别在哪
是的,让我详细解释一下实模式下的内存布局: 最低端内存布局(1MB以下(8088/8086)):准确地说是地址空间布局:
真正的初始位置是 0x00000000,从这里开始是中断向量表:
0x00000000 - 0x000003FF: 中断向量表 (1KB)
- 每个中断向量占 4 字节(2字节段地址 + 2字节偏移地址)
- 总共可以存储 256 个中断向量(1024/4=256)
- 比如 int 0x10 的向量位于 0x10*4 = 0x40 处
0x00000400 - 0x000004FF: BIOS 数据区 (256B)
- 存储 BIOS 的各种参数
- 如显示模式、键盘状态、时间等
0x00000500 - 0x00007BFF: 可用区域
- 操作系统或应用程序可以使用
0x00007C00 - 0x00007DFF: 引导扇区 (512B)
- BIOS 把**引导扇区(512字节)**加载到这个固定位置
- **这是历史遗留的规定,所有 PC 都遵循这个约定**
0x00007E00 - 0x0009FFFF: 可用区域
- 操作系统可以使用
0x000A0000 - 0x000BFFFF: 显存映射区域 (128KB)
- 用于文本模式和图形模式显示
0x000C0000 - 0x000C7FFF: 显卡 BIOS (32KB)
0x000C8000 - 0x000EFFFF: BIOS 映射区和其他设备
0x000F0000 - 0x000FFFFF: 系统 BIOS (64KB)
- BIOS 在 0xFFFF0 开始执行,然后进行初始化并最终加载引导扇区到 0x7C00
实际的物理内存:
- 中断向量表(0x00000000 - 0x000003FF)确实在物理内存中
- 它位于实际 RAM 的最开始部分
- 这部分内存是真实的物理内存条上的存储单元
- 开机时 BIOS 会初始化这个区域,填入中断向量
内存映射:
- 实际 RAM: 0x00000000 - 0x0009FFFF (640KB 基本内存)
- 中断向量表是在实际物理内存条上的,因为它需要在实际内存中因为 CPU 要频繁访问它
- 显存/ROM区域: 0x000A0000 - 0x000FFFFF (384KB 保留区域)
- 这部分不在内存条上
- 而是映射到显卡等硬件的内存空间
- 所以 0xA0000 以上的地址是映射到其他硬件的,不在内存条上
- 找到可引导设备后,BIOS 将其第一个扇区(512字节)加载到物理内存 0x7C00 处,而段寄存器是 0x7c0 这两个不一样啊?还有就是CS:IP = 0x7C0:start2_offset 这个应该是0x7c00吧
在实模式下,物理地址的计算公式是:
物理地址 = 段寄存器 × 16 + 偏移地址
物理地址 = 段寄存器 × 10h + 偏移地址
- 当 BIOS 把引导扇区加载到物理地址 0x7C00 时:
0x7C00 = 0x7C0 × 16 + 0
0x7C00 = 0x7C0 × 10h + 0
2.当我们设置 CS:IP = 0x7C0:start2_offset 时:
物理地址 = 0x7C0 × 16 + start2_offset
= 0x7C00 + start2_offset
这就是为什么:
- 段地址是 0x7C0
- 物理地址是 0x7C00
- 它们指向同一个物理位置
这也是为什么在代码中我们使用:
BOOTSEG = 0x7c0 # 段地址
jmpl start2 # 跳转到 0x7c0:start2
这样就能正确访问加载到 0x7C00 的代码。
ds es fs gs 都要填吗,空着不行吗? 在这个简单的引导程序中,不是所有的段寄存器都必须设置,但设置它们是一个好的编程习惯。让我解释为什么:
1.必需的段寄存器: movw $BOOTSEG, %ax movw %ax, %ds # 数据段寄存器 - 必需,因为要访问 msg 字符串 movw %ax, %es # 附加段寄存器 - 必需,因为 int 0x10 需要用 ES:BP 访问字符串
2.可选的段寄存器: movw %ax, %fs # 可以不设置 movw %ax, %gs # 可以不设置
原因: DS:用于访问数据(如 msg 字符串) ES:BIOS int 0x10 中断使用 ES:BP 指向字符串 FS/GS:在这个简单程序中没有使用,可以不设置
但是完整设置所有段寄存器有以下好处: 1.确保程序在任何环境下都能正确运行 2.防止其他程序遗留的段值造成问题 3.符合标准的初始化流程 4.为后续的代码扩展做好准备
所以虽然在这个简单程序中 FS 和 GS 不是必需的,但设置它们是一个好的编程实践。
到底是根据ds访问 msg还是es访问msg?
movw %ax, %ds # 这行其实在这个程序中不是必需的 那为什么还要设置 DS? 因为在一般的内存访问中,如果没有明确指定段寄存器,默认使用 DS。但在这个特定的 BIOS 中断中,它明确要求使用 ES:BP 来访问字符串。 所以严格来说,在这个简单的引导程序中,我们可以不设置 DS,代码依然能正常工作:
start2:
movw $BOOTSEG, %ax
movw %ax, %es # 只设置 ES 就够了
movw $msg, %bp # ES:BP 指向字符串
movw $0x01301, %ax
movw $0x0c, %bx
movw $12, %cx
movb $0, %dl
int $0x10
为什么需要引导扇区,为什么不直接加载操作系统然后直接运行?我理解是引导扇区相当于操作系统的函数签名,相当于调用部分,这样一来操作系统就相当于BIOS的一个调用函数了?根函数则是ALU模块,我理解对吗?感觉芯片和操作系统真就是自指?
这个理解不太准确,让我解释一下为什么需要引导扇区:
1.硬件限制原因:
- BIOS 只能读取存储设备的第一个扇区(512字节)
- 这是由早期 PC 架构决定的硬件限制
- BIOS 不知道如何加载完整的操作系统,因为不同操作系统的格式不同
2.引导扇区的作用: BIOS -> 引导扇区 -> 操作系统加载器(如GRUB) -> 操作系统内核 引导扇区像一个"翻译器"或"加载器"
- 它知道如何找到并加载特定操作系统的其余部分
- 它理解该操作系统的文件系统格式和加载要求
3.为什么不能直接加载:
- 操作系统可能很大(几百MB甚至几GB)
- 操作系统可能分布在磁盘的不同位置
- 不同操作系统有不同的加载要求
- BIOS 不可能理解所有操作系统的格式
4.实际过程: BIOS -> 引导扇区 -> 加载操作系统加载器(如GRUB) -> 加载操作系统内核 -> 初始化硬件(驱动初始化,BIOS也会初始化少量硬件) -> 启动操作系统服务
所以引导扇区不是操作系统的函数签名,而是一个独立的小程序,负责把控制权从 BIOS 转交给操作系统。它更像是一个"搬运工"或"中介",而不是函数调用接口。