从零构建 Linux 0.12:控制台初始化的底层实现与代码解析
在 Linux 0.12 内核的启动流程中,控制台初始化是连接硬件与用户的第一座桥梁。它通过识别显示器类型、配置显存映射和端口参数,为后续的内核信息输出与用户交互奠定基础。本文将基于完整源码,系统解析控制台初始化的核心逻辑与实现细节。
一、核心代码架构
控制台初始化的实现采用分层设计,形成了从入口调用到底层硬件操作的完整链路:
1. 初始化入口链
main.c:内核启动的起点,触发终端初始化流程
c
运行
#define __LIBRARY__
void main(void)
{
tty_init(); // 启动终端初始化
__asm__("int $0x80 \n\r"::); // 触发系统调用(预留接口)
__asm__ __volatile__(
"loop:\n\r"
"jmp loop" // 进入无限循环,等待中断
::);
}

tty_io.c:终端初始化中间层,简化调用逻辑
c
运行
#include <linux/tty.h>
void tty_init()
{
con_init(); // 直接调用控制台初始化函数
}
2. 底层硬件操作定义(asm/io.h)
该文件通过嵌入式汇编封装了硬件端口的读写操作,为控制台初始化提供硬件访问能力:
c
运行
/* 硬件IO端口访问的嵌入式汇编宏函数 */
/**
* 硬件端口字节输出
* @param[in] value 欲输出字节
* @param[in] port 端口
*/
#define outb(value, port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))
/**
* 硬件端口字节输入
* @param[in] port 端口
* @retval 返回读取的字节
*/
#define inb(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \
_v; \
})
/**
* 硬件端口字节输出(带延迟)
* 使用两条跳转语句来延迟一会儿
* @param[in] value 欲输出字节
* @param[in] port 端口
*/
#define outb_p(value, port) \
__asm__ ("outb %%al,%%dx\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:"::"a" (value),"d" (port))
/**
* 硬件端口字节输入(带延迟)
* 使用两条跳转语句来延迟一会儿
* @param[in] port 端口
* @retval 返回读取的字节
*/
#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ( \
"inb %%dx,%%al\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:":"=a" (_v):"d" (port)); \
_v; \
})
3. 控制台初始化核心实现(console.c)
该文件实现了显示器识别、参数配置和光标控制等核心功能:
c
运行
#include <linux/tty.h>
#include <asm/io.h> // 引入端口操作函数
// 从setup.s保存的内存区域读取硬件信息
#define ORIG_X (*(unsigned char *)0x90000) // 原始X坐标
#define ORIG_Y (*(unsigned char *)0x90001) // 原始Y坐标
#define ORIG_VIDEO_PAGE (*(unsigned short *)0x90004) // 视频页
#define ORIG_VIDEO_MODE ((*(unsigned short *)0x90006) & 0xff) // 视频模式
#define ORIG_VIDEO_COLS (((*(unsigned short *)0x90006) & 0xff00) >> 8) // 列数
#define ORIG_VIDEO_LINES ((*(unsigned short *)0x9000e) & 0xff) // 行数
#define ORIG_VIDEO_EGA_BX (*(unsigned short *)0x9000a) // EGA显卡参数
// 显示器类型定义
#define VIDEO_TYPE_MDA 0x10 /* 单色文本显示器 */
#define VIDEO_TYPE_CGA 0x11 /* CGA彩色显示器 */
#define VIDEO_TYPE_EGAM 0x20 /* EGA/VGA单色模式 */
#define VIDEO_TYPE_EGAC 0x21 /* EGA/VGA彩色模式 */
// 静态变量存储控制台参数
static unsigned char video_type; /* 显示器类型 */
static unsigned long video_num_columns; /* 列数 */
static unsigned long video_num_lines; /* 行数 */
static unsigned long video_mem_base; /* 显存基地址 */
static unsigned long video_mem_term; /* 显存结束地址 */
static unsigned long video_size_row; /* 每行字节数 */
static unsigned char video_page; /* 视频页 */
static unsigned short video_port_reg; /* 视频寄存器选择端口 */
static unsigned short video_port_val; /* 视频寄存器值端口 */
// 光标位置与屏幕范围控制变量
static unsigned long origin;
static unsigned long scr_end;
static unsigned long pos;
static unsigned long x,y;
static unsigned long top,bottom;
/*
* 定位光标位置函数
* 作用:根据坐标计算显存地址并更新光标位置
*/
static inline void gotoxy(int new_x, unsigned int new_y)
{
if (new_x > video_num_columns || new_y >= video_num_lines)
{
return;
}
x = new_x;
y = new_y;
pos = origin + y * video_size_row + (x << 1); // 每个字符占2字节(ASCII+属性)
}
/*
* 设置光标位置函数
* 作用:通过端口操作将光标移动到计算出的位置
*/
static inline void set_cursor()
{
cli(); // 关闭中断,确保端口操作原子性
outb_p(14, video_port_reg); // 发送高8位地址到端口
outb_p(0xff & ((pos - video_mem_base) >> 9), video_port_val);
outb_p(15, video_port_reg); // 发送低8位地址到端口
outb_p(0xff & ((pos - video_mem_base) >> 1), video_port_val);
sti(); // 恢复中断
}
/*
* 控制台初始化函数:
* 1. 读取setup.s保存的硬件信息
* 2. 识别显示器类型(MDA/CGA/EGA)
* 3. 配置对应的显存地址和端口参数
* 4. 初始化光标位置并在屏幕最后一行显示设备标识
*/
void con_init(void)
{
char *display_desc = "????"; // 显示器类型描述
char *display_ptr; // 显示位置指针
// 初始化基础参数
video_num_columns = ORIG_VIDEO_COLS;
video_size_row = video_num_columns * 2; // 每个字符占2字节(ASCII+属性)
video_num_lines = ORIG_VIDEO_LINES;
video_page = ORIG_VIDEO_PAGE;
// 判断是否为单色显示器(模式7)
if (ORIG_VIDEO_MODE == 7)
{
video_mem_base = 0xb0000; // 单色显存基地址
video_port_reg = 0x3b4; // 单色模式寄存器端口
video_port_val = 0x3b5; // 单色模式值端口
// 区分EGA单色模式与MDA
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
{
video_type = VIDEO_TYPE_EGAM;
video_mem_term = 0xb8000; // EGA单色显存结束地址
display_desc = "EGAm"; // 标识EGA单色模式
}
else
{
video_type = VIDEO_TYPE_MDA;
video_mem_term = 0xb2000; // MDA显存结束地址
display_desc = "*MDA"; // 标识MDA显示器
}
}
else // 彩色显示器
{
video_mem_base = 0xb8000; // 彩色显存基地址
video_port_reg = 0x3d4; // 彩色模式寄存器端口
video_port_val = 0x3d5; // 彩色模式值端口
// 区分EGA彩色模式与CGA
if ((ORIG_VIDEO_EGA_BX & 0xff) != 0x10)
{
video_type = VIDEO_TYPE_EGAC;
video_mem_term = 0xc0000; // EGA彩色显存结束地址
display_desc = "EGAc"; // 标识EGA彩色模式
}
else
{
video_type = VIDEO_TYPE_CGA;
video_mem_term = 0xba000; // CGA显存结束地址
display_desc = "*CGA"; // 标识CGA显示器
}
}
// 初始化屏幕范围参数
origin = video_mem_base;
scr_end = video_mem_base + video_num_lines * video_size_row;
top = 0;
bottom = video_num_lines;
// 恢复原始光标位置并更新硬件光标
gotoxy(ORIG_X, ORIG_Y);
set_cursor();
// 在屏幕最后一行显示显示器类型(方便调试)
display_ptr = ((char *)video_mem_base) + video_size_row - 8;
while (*display_desc)
{
*display_ptr++ = *display_desc++; // 写入字符ASCII码
display_ptr++; // 跳过属性位(默认使用原有属性)
}
}
4. 中断控制宏定义(system.h)
提供了中断开关等底层操作宏,保障端口操作的原子性:
c
运行
#ifndef _SYSTEM_H
#define _SYSTEM_H
// 中断控制宏定义
#define sti() __asm__("sti"::) // 开启中断
#define cli() __asm__("cli"::) // 关闭中断
#define nop() __asm__("nop"::) // 空操作
#define iret() __asm__("iret"::) // 中断返回
#endif
二、编译系统设计
控制台初始化代码的编译依赖于多层次的 Makefile 系统,确保各模块按正确顺序和参数编译:
1. 根目录 Makefile
makefile
AS := as
LD := ld -m elf_x86_64
LDFLAG := -Ttext 0x0 -s --oformat binary
all : linux.img # 最终目标:生成可启动镜像
# 拼接引导程序、设置程序和内核系统
linux.img : tools/build bootsect setup kernel/system
./tools/build bootsect setup kernel/system > $@
# 编译构建工具
tools/build : tools/build.c
gcc -o $@ $<
# 编译内核系统
kernel/system : kernel/head.s
cd kernel; make system; cd ..
# 编译引导扇区
bootsect : bootsect.o
$(LD) $(LDFLAG) -o $@ $<
bootsect.o : bootsect.s
$(AS) -o $@ $<
# 编译设置程序
setup : setup.o
$(LD) $(LDFLAG) -e _start_setup -o $@ $<
setup.o : setup.s
$(AS) -o $@ $<
# 清理编译产物
clean:
rm -f *.o bootsect setup tools/build linux.img
cd kernel; make clean; cd ..
cd tools; make clean; cd ..
# 运行模拟器
run:
qemu-system-i386 -fda linux.img # 使用qemu模拟x86环境
2. 内核目录 Makefile
makefile
# ---------- kernel/Makefile ----------
AS := as
CC := gcc
LD := ld
OBJCOPY := objcopy
ASFLAGS := --32 # 32位汇编
CFLAGS := -m32 -march=i386 -I../include -nostdinc -Wall \
-fno-builtin -fno-stack-protector -fno-pic -fno-pie # 禁用标准库和安全特性
OBJS := head.o main.o sched.o chr_drv/chr_drv.a # 内核组件
all: system # 目标:生成内核系统文件
# 链接内核组件
system: $(OBJS)
$(LD) -m elf_i386 -Ttext 0x0 -e startup_32 -nmagic -o system.elf $(OBJS)
$(OBJCOPY) -O binary system.elf system # 转换为二进制镜像
# 编译各模块
head.o: head.s
$(AS) $(ASFLAGS) -o $@ $<
main.o: main.c
$(CC) $(CFLAGS) -c -o $@ $<
sched.o: sched.c
$(CC) $(CFLAGS) -c -o $@ $<
chr_drv/chr_drv.a: chr_drv/*.c # 字符设备驱动库
cd chr_drv; make chr_drv.a; cd ..
# 清理内核编译产物
clean:
rm -f *.o system system.elf
cd chr_drv; make clean; cd ..
3. 字符设备目录 Makefile
makefile
# ---------- kernel/chr_drv/Makefile ----------
CC := gcc
AR := ar
CFLAGS := -m32 -I../../include -nostdinc -Wall -fomit-frame-pointer
OBJS := tty_io.o console.o # 终端与控制台目标文件
chr_drv.a: $(OBJS)
$(AR) rcs $@ $(OBJS) # 创建静态链接库
# 编译各目标文件
tty_io.o: tty_io.c
$(CC) $(CFLAGS) -c -o $@ $<
console.o: console.c
$(CC) $(CFLAGS) -c -o $@ $<
# 清理编译产物
clean:
rm -f *.o chr_drv.a
三、核心技术解析
1. 硬件信息获取机制
控制台初始化依赖于启动阶段保存的硬件信息,这些信息通过内存映射方式直接访问:
- 0x90000 开始的内存区域由 setup.s 程序预先填充 BIOS 提供的硬件参数
- 通过宏定义直接解析该区域数据,获取视频模式、行列数等关键信息
- 避免了初始化阶段频繁调用 BIOS 中断,提高启动效率
2. 多显示器适配策略
代码通过分层判断实现对不同显示器的兼容:
- 模式判断:通过 ORIG_VIDEO_MODE 区分单色(7)与彩色模式
- 端口配置:单色模式使用 0x3b4/0x3b5 端口,彩色模式使用 0x3d4/0x3d5 端口
- 显存划分:不同显示器的显存范围不同(MDA:0xb0000-0xb2000,EGA 彩色:0xb8000-0xc0000)
- 类型标识:通过 ORIG_VIDEO_EGA_BX 区分 EGA 与传统 MDA/CGA 设备
3. 光标控制实现
光标位置控制通过硬件端口操作完成,分为两个关键步骤:
- 地址计算:gotoxy () 函数根据坐标计算字符在显存中的位置(每个字符占 2 字节)
- 端口操作:set_cursor () 函数通过 outb_p () 向视频控制器发送光标位置指令
- 原子性保障:通过 cli ()/sti () 关闭 / 开启中断,确保端口操作不被打断
4. 端口操作设计
端口操作宏函数采用嵌入式汇编实现,具有以下特点:
- 使用寄存器约束符("a"、"d")自动分配寄存器,避免冲突
- 带延迟版本(outb_p/inb_p)通过空跳转指令提供硬件响应时间
- volatile 关键字确保端口操作不被编译器优化省略
- 统一封装硬件指令,简化上层代码对硬件的直接操作
四、运行与验证流程
- 编译镜像 :执行
make
命令,通过嵌套 Makefile 系统生成 linux.img - 启动模拟器 :执行
make run
启动 QEMU 模拟器 - 验证结果 :
- 屏幕最后一行显示显示器类型标识(如 "EGAm"、"*CGA")
- 光标定位到 BIOS 保存的原始位置
- 无硬件错误或显示异常
控制台初始化作为内核与硬件交互的早期环节,展示了 Linux 0.12 内核 "直接操作硬件、精简高效" 的设计哲学。从端口操作到显示器适配,每一处实现都体现了对硬件特性的深刻理解,为后续内核功能提供了基础的输入输出平台。