ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DIY操作系统(8):内核实现之屏幕打印——用TaoToken统一Key打通VGA文本模式调试链路

DIY操作系统(8):内核实现之屏幕打印——用TaoToken统一Key打通VGA文本模式调试链路 1. 从显存到光标VGA文本模式打印到底在解决什么问题内核开发走到屏幕打印这一步意味着你已经能加载内核、能跑 C 代码但屏幕上还是一片黑。VGA 文本模式是 x86 上最容易上手的输出方式它不需要显卡驱动不需要帧缓冲配置只要往物理地址 0xB8000 写数据字符就会出现在屏幕上。听起来简单但真正动手时会发现一堆细节显存段选择子怎么设、光标位置怎么读、换行和滚屏怎么处理、汇编和 C 怎么混合调用。我试过在 Windows 下用 MinGW 编译链接结果 bochs 连内核都加载不进去后来换到 CentOS 才稳定下来。所以这篇的环境建议直接用 Linux工具链用 gcc nasm ld调试用 QEMU 或 bochs 都行。VGA 文本模式默认 80x25 字符窗口每个字符占 2 字节低字节是 ASCII 码高字节是属性前景色、背景色、闪烁。显存总大小 80252 4000 字节从 0xB8000 开始。光标位置是一个一维索引范围 0 到 1999。它存在 CRT Controller 寄存器组的索引 0x0E 和 0x0F 里分别对应低 8 位和高 8 位。要读写这两个寄存器不能直接访问得通过 Address Register 和 Data Register 间接操作。端口地址取决于 Miscellaneous Output Register 的 I/OAS 位默认是 1所以 Address Register 端口是 0x3D4Data Register 端口是 0x3D5。打印一个字符的完整流程是备份寄存器现场读取当前光标位置判断字符类型回车、换行、退格、可见字符分别处理判断是否需要滚屏更新光标位置恢复现场。这些逻辑全部在 print.S 里用汇编实现main.c 里调用。下面我会把每个环节拆开给出可复制的代码片段和配置参数。这一篇的目标很明确让你在 QEMU 里启动自己写的内核屏幕上能打印出字符串光标能正确移动换行和滚屏都正常。同时我会演示怎么用 TaoToken 的统一 Key 接入 AI 辅助排查内核启动日志把调试效率提上来。TaoToken 的 API 地址是 https://taotoken.net/api官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content后面会给出具体的接入配置。2. TaoToken 前置统一 Key 与 API 通道怎么接入内核调试链路内核开发最头疼的不是写代码而是排查启动日志。QEMU 的串口输出、bochs 的调试信息、屏幕上的乱码往往需要反复对照。如果能把内核启动日志通过串口或文件导出再交给 AI 辅助分析定位问题的速度会快很多。TaoToken 在这里的角色是提供一个统一的 API 通道让你用同一个 Key 调用多种模型不需要为每个模型单独申请账号和配置环境。你需要先拿到 TaoToken 的 API Key。访问 https://taotoken.net/api-keys 创建 Key然后保存好。这个 Key 后面会用在 curl 请求或者 Python 脚本里。TaoToken 的 API 地址是 https://taotoken.net/api兼容 OpenAI 风格的接口所以你可以用标准的 chat completions 格式调用。在内核调试场景里我通常这样做QEMU 启动时加-serial file:serial.log把串口输出写到文件内核里用outb往串口端口 0x3F8 写日志。启动完成后用脚本读取 serial.log把内容发给 TaoToken 的模型对话接口让 AI 帮我分析有没有异常。模型对话的入口是 https://taotoken.net/api/chat/completions你需要带上 Authorization 头格式是Bearer YOUR_KEY。如果你打算长期做内核开发建议用 Coding Plan入口在 https://taotoken.net/coding-plan它适合需要频繁调用 API 做代码分析和调试的场景。接入文档在 https://taotoken.net/doc里面有详细的参数说明和示例。API Keys 管理页面在 https://taotoken.net/api-keys可以随时查看和轮换 Key。这里要强调一点TaoToken 只是提供 API 通道不替代你的编辑器或编译工具链。你还是在本地写代码、编译、用 QEMU 启动只是把日志分析这一步交给 AI。下面给出一个可复制的配置片段用于在脚本里调用 TaoToken 的模型对话接口。# 保存为 analyze_log.sh # 用法./analyze_log.sh serial.log TAOTOKEN_KEY你的API Key LOG_FILE$1 if [ ! -f $LOG_FILE ]; then echo 日志文件不存在: $LOG_FILE exit 1 fi LOG_CONTENT$(cat $LOG_FILE) curl -s https://taotoken.net/api/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_KEY \ -d { \model\: \gpt-4o-mini\, \messages\: [ {\role\: \system\, \content\: \你是一个操作系统内核调试助手请分析以下内核启动日志指出可能的错误和异常。\}, {\role\: \user\, \content\: \$LOG_CONTENT\} ], \temperature\: 0.2 }这个脚本会把 serial.log 的内容发给 TaoToken模型会返回分析结果。你可以根据返回内容判断是显存映射错了、光标计算错了还是滚屏逻辑有问题。实测下来这种方式比人工逐行看日志快很多尤其是日志量大的时候。如果你用的是 Claude Code 或者 Cline 这类工具也可以把 TaoToken 的 Base URL 配进去。Base URL 填 https://taotoken.net/apiKey 填你的 TaoToken KeyModel ID 填你需要的模型名。这样你在写内核代码时AI 可以直接读取你的项目文件给出更精准的建议。Cline MCP 的配置里Base URL、Key、Model ID 三件套都要写全缺一不可。3. 可复制配置VGA 寄存器、print.S 与 main.c 的完整实现这一节给出可以直接复制到项目里的代码。目录结构参考如下boot/mbr.S # MBR 代码 boot/loader.S # 内核加载器 boot/include/boot.inc # 公用宏定义 kernel/main.c # 内核主文件 lib/kernel/print.S # 屏幕打印汇编实现 lib/kernel/print.h # 打印函数声明编译顺序mbr.S 单独编译为 bin写入硬盘第一扇区loader.S 单独编译为 bin写入第三扇区print.S 编译为 .omain.c 编译为 .o链接 main.o 和 print.o 生成 kernel.bin写入第十扇区。先看 VGA 寄存器的端口定义。在 print.S 开头加上这些常量; lib/kernel/print.S ; VGA 文本模式相关端口 VIDEO_SEGMENT equ 0xB800 ; 显存段选择子实际地址 0xB8000 CRT_ADDR_PORT equ 0x3D4 ; Address Register 端口 CRT_DATA_PORT equ 0x3D5 ; Data Register 端口 CURSOR_HIGH equ 0x0E ; 光标高 8 位索引 CURSOR_LOW equ 0x0F ; 光标低 8 位索引然后是 put_char 函数。它的调用规则是压栈一个字符的 ASCII 码然后 call put_char。函数内部用 pushad 备份 8 个 32 位寄存器共 32 字节加上返回地址 4 字节所以待打印字符在栈顶偏移 36 字节的位置。; put_char: 打印一个字符 ; 调用方式push ascii; call put_char global put_char put_char: pushad ; 备份 EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI ; 设置 GS 为显存段选择子 mov ax, VIDEO_SEGMENT mov gs, ax ; 获取当前光标位置 ; 先读高 8 位 mov dx, CRT_ADDR_PORT mov al, CURSOR_HIGH out dx, al mov dx, CRT_DATA_PORT in al, dx mov ah, al ; 高 8 位存入 ah ; 再读低 8 位 mov dx, CRT_ADDR_PORT mov al, CURSOR_LOW out dx, al mov dx, CRT_DATA_PORT in al, dx ; 低 8 位存入 al ; 此时 ax 中保存了光标位置 mov bx, ax ; bx 光标位置 ; 获取待打印字符 mov ecx, [esp 36] ; 36 32(pushad) 4(返回地址) mov cl, [esp 36] ; 取低 8 位 ASCII ; 判断字符类型 cmp cl, 0x0D ; 回车 CR je .is_cr cmp cl, 0x0A ; 换行 LF je .is_lf cmp cl, 0x08 ; 退格 BS je .is_backspace jmp .put_other .is_backspace: ; 光标回退一格用空格覆盖 cmp bx, 0 je .put_char_done ; 已经在开头不处理 dec bx mov byte [gs:bx*2], mov byte [gs:bx*21], 0x07 jmp .set_cursor .is_cr: ; 回车光标移到行首 mov ax, bx mov dx, 0 mov cx, 80 div cx ; ax 行号, dx 列号 sub bx, dx ; bx 行首位置 jmp .set_cursor .is_lf: ; 换行光标移到下一行同一列 add bx, 80 jmp .check_roll .put_other: ; 可见字符写入显存 mov byte [gs:bx*2], cl mov byte [gs:bx*21], 0x07 ; 黑底白字 inc bx .check_roll: ; 判断是否需要滚屏 cmp bx, 2000 jl .set_cursor call roll_screen sub bx, 80 ; 滚屏后光标上移一行 .set_cursor: ; 设置光标位置 mov dx, CRT_ADDR_PORT mov al, CURSOR_HIGH out dx, al mov dx, CRT_DATA_PORT mov al, bh out dx, al mov dx, CRT_ADDR_PORT mov al, CURSOR_LOW out dx, al mov dx, CRT_DATA_PORT mov al, bl out dx, al .put_char_done: popad retroll_screen 函数负责滚屏把第 1 到 24 行的内容复制到第 0 到 23 行最后一行用空格填充。; roll_screen: 滚屏 roll_screen: pushad mov ax, VIDEO_SEGMENT mov gs, ax ; 复制 1~24 行到 0~23 行 mov ecx, 80 * 24 ; 要复制的字符数 mov esi, 80 * 2 ; 源偏移第 1 行开始 mov edi, 0 ; 目标偏移第 0 行开始 .copy_loop: mov ax, [gs:esi] mov [gs:edi], ax add esi, 2 add edi, 2 loop .copy_loop ; 最后一行填充空格 mov ecx, 80 mov edi, 80 * 24 * 2 .fill_loop: mov word [gs:edi], 0x0720 ; 空格 属性 add edi, 2 loop .fill_loop popad retprint.h 里声明函数// lib/kernel/print.h #ifndef __LIB_KERNEL_PRINT_H #define __LIB_KERNEL_PRINT_H void put_char(char c); void put_str(const char* str); #endifmain.c 里调用// kernel/main.c #include print.h void main(void) { put_str(Hello, Kernel!\n); put_str(VGA text mode printing works.\n); put_str(Line 3: testing scroll.\n); while (1); }put_str 可以用汇编实现循环调用 put_char; put_str: 打印字符串 ; 调用方式push 字符串地址; call put_str global put_str put_str: pushad mov esi, [esp 36] ; 字符串地址 xor ecx, ecx .str_loop: mov cl, [esi] cmp cl, 0 je .str_done push ecx call put_char add esp, 4 inc esi jmp .str_loop .str_done: popad ret编译链接命令nasm -f elf -o print.o lib/kernel/print.S gcc -m32 -c -o main.o kernel/main.c ld -m elf_i386 -Ttext 0x1000 -o kernel.bin main.o print.o注意链接地址要和 loader.S 里加载内核的地址一致。如果用的是 0x1000那-Ttext 0x1000就要对应上。4. 验证请求与成功结果QEMU 启动后屏幕输出与日志分析代码写完后用 QEMU 启动验证。假设你已经把 mbr、loader、kernel 写入硬盘镜像 hd.img启动命令qemu-system-i386 -hda hd.img -serial file:serial.log -display curses-display curses可以在终端里直接看到 VGA 文本输出-serial file:serial.log把串口日志写到文件。如果一切正常屏幕上会显示Hello, Kernel! VGA text mode printing works. Line 3: testing scroll.光标会停在第四行行首。如果你连续打印超过 25 行会看到滚屏效果第一行消失所有内容上移一行最后一行是新的内容。验证光标位置是否正确可以在打印完字符串后用 QEMU 的 monitor 查看显存内容。启动时加-monitor stdio然后在 QEMU 命令行里输入xp /2000xb 0xb8000可以看到显存里的 ASCII 码和属性字节。比如Hello对应的字节是0x48 0x07 0x65 0x07 0x6c 0x07 0x6c 0x07 0x6f 0x07。如果屏幕没有输出先检查显存段选择子。VIDEO_SEGMENT 应该是 0xB800对应物理地址 0xB8000。如果写成了 0xB8000段选择子会出错。再检查光标读取逻辑先往 0x3D4 写索引 0x0E再从 0x3D5 读高 8 位然后往 0x3D4 写 0x0F从 0x3D5 读低 8 位。顺序不能反。串口日志这边你可以在内核里加一个简单的串口输出函数把关键信息写到 0x3F8。比如; 初始化串口 mov dx, 0x3F9 mov al, 0x00 out dx, al mov dx, 0x3FB mov al, 0x80 out dx, al mov dx, 0x3F8 mov al, 0x03 out dx, al mov dx, 0x3F9 mov al, 0x00 out dx, al mov dx, 0x3FB mov al, 0x03 out dx, al mov dx, 0x3FA mov al, 0xC7 out dx, al mov dx, 0x3FC mov al, 0x0B out dx, al然后写一个 put_serial 函数把字符写到 0x3F8。这样 serial.log 里就会有内核启动日志。启动完成后运行前面写的 analyze_log.sh把日志发给 TaoToken 分析。如果模型返回“未发现异常”说明启动流程正常如果返回“光标位置计算可能越界”那就去检查 roll_screen 里的边界判断。实测下来QEMU 的-display curses和-serial file:组合最方便不需要图形界面适合在服务器上调试。如果你用 bochs配置里要打开display_library: term和serial: file:serial.log效果类似。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth 报错接入 TaoToken 做日志分析时最容易碰到几类报错。下面逐个说明原因和解决办法。401 Unauthorized这个报错说明 API Key 不对或者没带上。检查你的请求头里有没有Authorization: Bearer YOUR_KEYKey 有没有复制完整。TaoToken 的 Key 在 https://taotoken.net/api-keys 页面创建创建后只显示一次要保存好。如果 Key 泄露了可以在同一页面删除重建。另外注意Key 前面不要多加空格Bearer 和 Key 之间只有一个空格。local proxy failed这个报错通常出现在你本地配置了代理但代理不可用或者配置错了。TaoToken 的 API 地址是 https://taotoken.net/api直接访问即可不需要额外代理。如果你在公司网络里检查一下环境变量http_proxy和https_proxy有没有设置成不可用的地址。用curl -v https://taotoken.net/api/chat/completions可以看到详细的连接过程如果卡在代理握手就把代理环境变量清掉再试。reading choices 报错这个报错一般出现在解析响应时。TaoToken 返回的是标准 JSON结构里choices是一个数组每个元素有message.content。如果你的脚本里直接读response.choices[0].message.content但响应里没有choices字段就会报错。先打印完整的响应体看看可能是模型名写错了或者请求体格式不对。比如 model 字段填了不存在的模型名API 会返回错误信息而不是 choices。检查 model 字段是否和 TaoToken 文档里列出的一致。OAuth 相关报错如果你用的是 Claude Code 或者 Cline 这类工具配置 TaoToken 时可能会碰到 OAuth 报错。这类工具通常需要你填 Base URL、Key、Model ID 三件套。Base URL 填 https://taotoken.net/apiKey 填 TaoToken 的 KeyModel ID 填你需要的模型名。如果工具提示 OAuth 失败检查一下是不是把 Base URL 填成了带/v1的地址。TaoToken 的 API 地址是 https://taotoken.net/api不需要额外加/v1。另外有些工具会缓存旧的认证信息清一下缓存再试。还有一个常见问题是内核本身的问题不是 API 的问题。比如屏幕打印乱码可能是属性字节写错了。属性字节 0x07 是黑底白字0x0F 是黑底亮白字0x1F 是蓝底白字。如果你看到字符颜色不对检查属性字节。如果字符位置不对检查光标计算里的除法和取模。80 列的情况下行号 位置 / 80列号 位置 % 80。回车处理时sub bx, dx把光标移到行首换行处理时add bx, 80移到下一行同一列。如果 QEMU 启动后直接重启或者卡住先检查 mbr 和 loader 的加载地址。mbr 在 0x7C00loader 在 0x9000 或者你指定的地址内核在 0x1000 或者你指定的地址。链接脚本里的-Ttext要和 loader 里跳转的地址一致。用objdump -h kernel.bin可以看到各个段的地址。6. 语义一致 CTA把内核调试日志交给 TaoToken 持续分析内核开发是一个反复调试的过程每次修改 print.S 或者 main.c 后都需要重新编译、启动 QEMU、查看输出。如果能把每次的启动日志自动发给 TaoToken 分析就能快速定位回归问题。你可以在 Makefile 里加一个 target编译完成后自动运行 QEMU 并分析日志。run: kernel.bin qemu-system-i386 -hda hd.img -serial file:serial.log -display curses -no-reboot ./analyze_log.sh serial.log kernel.bin: main.o print.o ld -m elf_i386 -Ttext 0x1000 -o kernel.bin main.o print.o main.o: kernel/main.c gcc -m32 -c -o main.o kernel/main.c print.o: lib/kernel/print.S nasm -f elf -o print.o lib/kernel/print.S这样每次make run都会自动分析日志。如果你需要更深入的代码分析比如让 AI 帮你检查 print.S 里的寄存器使用有没有问题可以用 Coding Plan入口在 https://taotoken.net/coding-plan。它适合长期做内核开发、需要频繁调用 API 的场景。模型对话的入口在 https://taotoken.net/api/chat/completions你可以直接用 curl 调用也可以集成到自己的脚本里。接入文档在 https://taotoken.net/doc里面有完整的参数说明和示例代码。API Keys 管理在 https://taotoken.net/api-keys建议定期轮换 Key。最后给一个实用技巧在 print.S 里加一个调试开关通过串口输出当前光标位置和待打印字符的 ASCII 码。这样当屏幕输出异常时serial.log 里会有详细的数据TaoToken 分析起来更准确。比如在 put_char 开头加; 调试输出打印光标位置和字符 push eax push ebx mov dx, 0x3F8 mov al, C out dx, al mov al, out dx, al mov al, bh out dx, al mov al, bl out dx, al mov al, out dx, al pop ebx pop eax这样 serial.log 里会看到类似C00 05的记录表示光标在第 0 行第 5 列。结合 TaoToken 的分析能快速判断是光标计算错了还是显存写入错了。内核开发没有捷径但好的调试工具链能省很多时间。
返回列表