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单色模式与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. 多显示器适配策略

代码通过分层判断实现对不同显示器的兼容:

  1. 模式判断:通过 ORIG_VIDEO_MODE 区分单色(7)与彩色模式
  2. 端口配置:单色模式使用 0x3b4/0x3b5 端口,彩色模式使用 0x3d4/0x3d5 端口
  3. 显存划分:不同显示器的显存范围不同(MDA:0xb0000-0xb2000,EGA 彩色:0xb8000-0xc0000)
  4. 类型标识:通过 ORIG_VIDEO_EGA_BX 区分 EGA 与传统 MDA/CGA 设备

3. 光标控制实现

光标位置控制通过硬件端口操作完成,分为两个关键步骤:

  • 地址计算:gotoxy () 函数根据坐标计算字符在显存中的位置(每个字符占 2 字节)
  • 端口操作:set_cursor () 函数通过 outb_p () 向视频控制器发送光标位置指令
  • 原子性保障:通过 cli ()/sti () 关闭 / 开启中断,确保端口操作不被打断

4. 端口操作设计

端口操作宏函数采用嵌入式汇编实现,具有以下特点:

  • 使用寄存器约束符("a"、"d")自动分配寄存器,避免冲突
  • 带延迟版本(outb_p/inb_p)通过空跳转指令提供硬件响应时间
  • volatile 关键字确保端口操作不被编译器优化省略
  • 统一封装硬件指令,简化上层代码对硬件的直接操作

四、运行与验证流程

  1. 编译镜像 :执行 make 命令,通过嵌套 Makefile 系统生成 linux.img
  2. 启动模拟器 :执行 make run 启动 QEMU 模拟器
  3. 验证结果
    • 屏幕最后一行显示显示器类型标识(如 "EGAm"、"*CGA")
    • 光标定位到 BIOS 保存的原始位置
    • 无硬件错误或显示异常

控制台初始化作为内核与硬件交互的早期环节,展示了 Linux 0.12 内核 "直接操作硬件、精简高效" 的设计哲学。从端口操作到显示器适配,每一处实现都体现了对硬件特性的深刻理解,为后续内核功能提供了基础的输入输出平台。

相关推荐
智能物联实验室14 分钟前
如何低门槛自制Zigbee 3.0温湿度计?涂鸦上新开发包,开箱即用、完全开源
嵌入式硬件·开源·硬件工程
时间裂缝里的猫-O-32 分钟前
@Linux问题 :bash fork Cannot allocate memory 错误分析与解决方案
linux·chrome·bash
派拉软件38 分钟前
微软AD国产化替换倒计时——不是选择题,而是生存题
网络·安全·microsoft·目录管理·微软ad替换·身份与访问控制管理iam
躺不平的小刘1 小时前
从YOLOv5到RKNN:零冲突转换YOLOv5模型至RK3588 NPU全指南
linux·python·嵌入式硬件·yolo·conda·pyqt·pip
愚昧之山绝望之谷开悟之坡1 小时前
| `cat /etc/os-release` | 发行版详细信息(如 Ubuntu、CentOS) |
linux·ubuntu·centos
草莓熊Lotso1 小时前
【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day10
c语言·开发语言·经验分享·算法·强化
明天见~~2 小时前
Linux下的网络编程
linux·运维·网络
NEXU52 小时前
Linux:网络层IP协议
linux·网络·tcp/ip
Aczone282 小时前
Linux 软件编程(九)网络编程:IP、端口与 UDP 套接字
linux·网络·网络协议·tcp/ip·http·c#
倔强的石头_3 小时前
【Linux指南】Makefile入门:从概念到基础语法
linux