12.从零开始写LINUX内核--控制台初始化
从零构建 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单色模式与MDAif ((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彩色模式与CGAif ((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 binaryall : linux.img # 最终目标:生成可启动镜像# 拼接引导程序、设置程序和内核系统
linux.img : tools/build bootsect setup kernel/system./tools/build bootsect setup kernel/system > $@# 编译构建工具
tools/build : tools/build.cgcc -o $@ $<# 编译内核系统
kernel/system : kernel/head.scd 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.imgcd 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.elfcd chr_drv; make clean; cd ..
3. 字符设备目录 Makefile
makefile
# ---------- kernel/chr_drv/Makefile ----------
CC := gcc
AR := ar
CFLAGS := -m32 -I../../include -nostdinc -Wall -fomit-frame-pointerOBJS := 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 内核 "直接操作硬件、精简高效" 的设计哲学。从端口操作到显示器适配,每一处实现都体现了对硬件特性的深刻理解,为后续内核功能提供了基础的输入输出平台。