图像模式的初始化与内存寻址模式的切换

 

内核启动程序

检测

通过query_list函数获取intel_speedstep 信息,检查cpu信息 并将信息储存在boot_params

speedstep技术 通过降低cpu运行主频来达到降低功耗,intel为笔记本cpu开发

通过 query_apm_bios 获取高级电源管理信息,此函数依旧通过int 0x15来使用中断, 但是多添加一个ah=0x53来确保APM的安装,中断处理结束后,函数检查PM标志位(0x504d),carry 标志位(如果APM使用,0x0),cx寄存器(0x02)提供保护模式接口

再次使用0x15中断,此时使用ax=0x5304 ,断开APM接口 ,连接32位保护模式接口,最后,将boot_params.apm_bios_info用从BIOS里包含的数据填充完整

只有当CONFIG_APM或者CONFIG_APM_MODULE被设置时query_apm_bios才被使用.

#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
    query_apm_bios();
#endif

最后是 query_edd 函数,从BIOS里检查Enhanced Disk Drive信息

querry_edd实现流程

  1. 从内核的命令行读取edd选项,如果关闭,则直接返回
  2. 如果EDD可用,querry_edd遍历所有的BIOS支持的因i教案,检测edd信息
    for (devno = 0x80; devno < 0x80+EDD_MBR_SIG_MAX; devno++) {
        if (!get_edd_info(devno, &ei) && boot_params.eddbuf_entries < EDDMAXNR) {
            memcpy(edp, &ei, sizeof ei);
            edp++;
            boot_params.eddbuf_entries++;
        }
        ...
        ...
        ...
        }
    

    第一个硬件序列号为0x80,EDD_MBR_SIG_MAXmarco为16,从edd_info里获取信息,.

    get_edd_info通过0x13中断 ah = 0x41来检查是否EDD被使用,get_edd_info

    再次使用0x13中断,这次ah=0x48 si保存EDD储存数据的缓冲地址

视窗模式的初始化 ,以及向保护模式的转变

视窗模式从set_video函数开始, arch/x86/boot/video.c (源码)

获取boot_params.hdr结构体

u16 mode = boot_params.hdr.vid_mode;

vid_mode在引导装载程序中被填充,同时在引导协议中也能看到对应解释

Offset    Proto    Name        Meaning
/Size
01FA/2    ALL        vid_mode    Video mode control

vga=<mode>
    <mode> here is either an integer (in C notation, either
    decimal, octal, or hexadecimal) or one of the strings
    "normal" (meaning 0xFFFF), "ext" (meaning 0xFFFE) or "ask"
    (meaning 0xFFFD).  This value should be entered into the
    vid_mode field, as it is used by the kernel before the command
    line is parsed.

​ 因此,我们将vga选项加入grub设置文件中,由此将该选项传递给内核命令行

该选项被描述为有不同的值,可以为整形的值(0xffff)或者被要求参数asked

如果是asked,在quem运行时如下

po1

选择一个视图模式.接下来深入讨论实现

对于初见内核,我们需要一些其他的东西

内核数据类型

在使用像u64这样的类型之前,在代码中有其他的数据类型

Type char short int long u8 u16 u32 u64
Size 1 2 4 8 1 2 4 8

堆处理 API

在获取boot_params.hdr后,我们看到RESET_HEAP函数,该函数其实是一个宏定义

#define RESET_HEAP() ((void *)( HEAP = _end ))

还有GET_HEAP

#define GET_HEAP(type, n) \
    ((type *)__get_heap(sizeof(type),__alignof__(type),(n)))

实现堆定位, 通过向__get_heap传递三个参数

  • 数据类型的大小
  • 该数据变量的对齐方式
  • n,定位的数量
static inline char *__get_heap(size_t s, size_t a, size_t n)
{
    char *tmp;

    HEAP = (char *)(((size_t)HEAP+(a-1)) & ~(a-1));
    tmp = HEAP;
    HEAP += s*n;
    return tmp;
}

GET_HEAP使用

saved.data = GET_HEAP(u16, saved.x * saved.y);

在get_heap中 被赋值为_end的HEAP被重新赋值为对齐后的值,对齐用到的变量是参数a,即alignof__(type)

HEAP跳转到分配内存区的末尾,返回原来的值

最后是heap_free

static inline bool heap_free(size_t n)
{
    return (int)(heap_end - HEAP) >= (int)n;
}

HEAP值减去heap_end,如果有足够值,返回1

设置视频模式

正式转入视频模式初始化,在RESET_HEAP后,store_mode_paramsboot_params.screen_info中获取video mode的参数.这些定义在头文件include/uapi/linux/screen_info.h

store_mode_params函数,首先调用store_cursor_position,从cursor中获取信息,并储存.

store_cursor_position初始化两个biosreg变量 AH值为0x3 启动0x10号bios中断, 成功执行后,,返回视频的行列,被储存在 boot_params.screen_info 结构体的orig_xorig_y

之后,store_mode_params检查当前的视频模式,设置视频参数,在BIOS将控制转给引导向量后,下面的地址为video内存准备

0xB000:0x0000     32 Kb     Monochrome Text Video Memory
0xB800:0x0000     32 Kb     Color Text Video Memory

如果当前的video mode 是MDA ,HGC ,VGA Monochrome模式我们将video_segment值放到0xb000

或者是color模式,放到0xb800.

在设置视频内存段后,font size需要被存进boot_params.screen_info.orig_video_points

set_fs(0);
font_size = rdfs16(0x485);
boot_params.screen_info.orig_video_points = font_size;

将fs设置为0后,从0x485获取size,并储存

x = rdfs16(0x44a);
y = (adapter == ADAPTER_CGA) ? 25 : rdfs8(0x484)+1;

0x44a处获取列的数量 从0x484处获取行的数量

存进boot_params.screen_info.orig_video_colsboot_params.screen_info.orig_video_lines中.完成store_mode_params.

save_screen函数只是保存了屏幕到堆内存中,该函数保存了所有上一个函数中获取到的信息.并将其放入saved_screen结构体中

static struct saved_screen {
    int x, y;
    int curx, cury;
    u16 *data;
} saved;

然后检查是否堆空间有足够的释放的空间给它.

if (!heap_free(saved.x*saved.y*sizeof(u16)+512))
        return;

如果空间足够 ,分配空间,将saved_screen储存在里面

下一个调用 probe_cards(0)arch/x86/boot/video-mode.c

遍历所有的video_cards ,收集这些cards提供的模式参数

for (card = video_cards; card < video_cards_end; card++) {
  /* collecting number of modes here */
}

但是video_cred不是在哪里都被声明,因此,每一个视频模式在x86内核中都有一个定义

static __videocard video_vga = {
    .card_name    = "VGA",
    .probe        = vga_probe,
    .set_mode    = vga_set_mode,
};

#define __videocard struct card_info __attribute__((used,section(".videocards")))

card_info

struct card_info {
    const char *card_name;
    int (*set_mode)(struct mode_info *mode);
    int (*probe)(void);
    struct mode_info *modes;
    int nmodes;
    int unsafe;
    u16 xmode_first;
    u16 xmode_n;
};

arch/x86/boot/setup.ld 连接脚本里

.videocards    : {
        video_cards = .;
        *(.videocards)
        video_cards_end = .;
    }

这意味着vide_cards只是一个内存地址,所有card_info结构体都被放在该段下.因此可以使用一个循环来遍历所有.

probe_cards执行后,我们会有一堆像 static __videocard video_vga 一样的结构体填充

在结束probe_cards后,转向set_video主循环,

,进入无限循环,试图找到video_mode带有的set_mode函数,如果没有将vid_mode=asked传递给内核命令 或者video_mode没有被定义则打印出菜单.

set_mode函数在video-mode.c 被定义,只需要一个参数 , 即modemode代指视频模式对应的值.

函数检查mode后,调用raw_set_mode函数,raw_set_mode调用被选择的card的set_mode函数.我们从card_info结构体中获取到这个函数.

例如vga

static int vga_set_mode(struct mode_info *mode)
{
    vga_set_basic_mode();

    force_x = mode->x;
    force_y = mode->y;

    switch (mode->mode) {
    case VIDEO_80x25:
        break;
    case VIDEO_8POINT:
        vga_set_8font();
        break;
    case VIDEO_80x43:
        vga_set_80x43();
        break;
    case VIDEO_80x28:
        vga_set_14font();
        break;
    case VIDEO_80x30:
        vga_set_80x30();
        break;
    case VIDEO_80x34:
        vga_set_80x34();
        break;
    case VIDEO_80x60:
        vga_set_80x60();
        break;
    }
    return 0;
}

每个设置视频模式的函数通过设置ah寄存器的值并使用0x10BIOS中断.

设定好mode后,将其传递给 boot_params.hdr.vid_mode.

接下来,vesa_store_edid调用,这个函数简单地储存EDID(Extended Display Identification Data)信息.在调用 store_mode_params后,最终do_restore设置

屏幕被保存为早期的状态

接下来,进入保护模式

进入保护模式前最后的准备

arch/x86/boot/main.c下一个进入的函数是go_to_protected_mode

该函数定义在arch/x86/boot/pm.c ,包含了做最后准备时用到的一些函数,

如果realmode_switch_hook不为空且 NMI禁用,则调用该钩子函数.realmode_switch函数提供了一个指针指向16位实模式子例程,禁用中断屏蔽,检查钩子后,不可中断屏蔽被禁用

asm volatile("cli");
outb(0x80, 0x70);    /* Disable NMI */
io_delay();

首先是内联汇编”cli”,清除IF ,外部中断被阻止.下一行禁用NMI

NMI

中断是一个由外部设备发出给cpu的信号,.在收到这样的信号后cpu暂停当前例程,保存当前状态,把控制流交给中断处理程序.处理完成后恢复原状态,继续执行.

而NMI是必须执行的中断,他们不能被忽略,并且经常用于无法恢复的硬件错误.

回到代码

第二行中,将0x80写为0x70(CMOS地址寄存器) 然后是io_delay,

static inline void io_delay(void)
{
    const u16 DELAY_PORT = 0x80;
    asm volatile("outb %%al,%0" : : "dN" (DELAY_PORT));
}

向0x80端口输出的数据需要演示1毫秒,因此我们可以卸任盒子到0x80端口,在这个延时期间,钩子函数完成,进入到下一个函数

下一个函数是enable_a20 启用A_20线,该函数定义在arch/x86/boot/a20.c 他尝试使用不同的方法打开A20门.

首先通过a20_test_short函数检查是否A20线已经可用.由a20_test实现

static int a20_test(int loops)
{
    int ok = 0;
    int saved, ctr;

    set_fs(0x0000);
    set_gs(0xffff);

    saved = ctr = rdfs32(A20_TEST_ADDR);

        while (loops--) {
        wrfs32(++ctr, A20_TEST_ADDR);
        io_delay();    /* Serialize and make delay constant */
        ok = rdgs32(A20_TEST_ADDR+0x10) ^ ctr;
        if (ok)
            break;
    }

    wrfs32(saved, A20_TEST_ADDR);
    return ok;
}

0x0000放入fs 0xffff放入GS然后从通过wrfs32函数读取fs:A20_TEST_ADDR值,,延时1ms,从GS寄存器里读取值放到 A20_TEST_ADDR+0x10里.如果被A20未被使用,会造成覆盖,否则,A20线已经启用

如果A线禁用,我们通过不同的方式来启用它,例如调用AH=0x20410x15BIOS中断,

如果无法成功启用A20线,打印出错误信息,使用函数diearch/x86/boot/header.S

die:
    hlt
    jmp    die
    .size    die, .-die

A20被成功启用,调用reset_coprocessor

outb(0, 0xf0);
outb(0, 0xf1);

通过将0写到0xf0清除数学处理器 通过将0写到0xf1重置

mask_all_interrupts调用

outb(0xff, 0xa1);       /* Mask all interrupts on the secondary PIC */
outb(0xfb, 0x21);       /* Mask all but cascade on the primary PIC */

这段代码在二次PIC和主PIC(特别是IRQ2)上掩盖所有的中断

准备完成

建立中断描述符表

setup_idt函数

static void setup_idt(void)
{
    static const struct gdt_ptr null_idt = {0, 0};
    asm volatile("lidtl %0" : : "m" (null_idt));
}

设置中断描述符表,现在为止 IDT还没有被安装,但是现在我们加载IDT都是通过lidt指令,(null_idt)包含IDT的大小和地址,但是现在他们都还是0,null_idt是gdt_ptr结构体类型

struct gdt_ptr {
    u16 len;
    u32 ptr;
} __attribute__((packed));

包含16位长的len 32位的指针 .__attribute__((packed))gdt_ptr大小是需要的最小值.即,6byte

设置全局描述符表

setup_gdt函数用于建立GDT,下面是boot_gdt数组的定义,包含了对三个段的定义

static const u64 boot_gdt[] __attribute__((aligned(16))) = {
    [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
    [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
    [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
};

对于代码段,数据段和TSS段,我们不会使用任务状态段,\

对于该数组有__attribute__((aligned(16)))描述符,该结构体会按照16byte对齐

例如

#include <stdio.h>

struct aligned {
    int a;
}__attribute__((aligned(16)));

struct nonaligned {
    int b;
};

int main(void)
{
    struct aligned    a;
    struct nonaligned na;

    printf("Not aligned - %zu \n", sizeof(na));
    printf("Aligned - %zu \n", sizeof(a));

    return 0;
}
$ gcc test.c -o test && test
Not aligned - 4
Aligned - 16

GDT_ENTRY_BOOT_CS 在这里有index-2

GDT_ENTRY_BOOT_DSGDT_ENTRY_BOOT_CS + 1

从2开始,因为第一个是强制性的NUll

第二个是未被使用的-1

GDT_ENTRY是一个宏 用 获取信息构建一个GDT入口

例如,当观察代码段时,GDT_entry使用以下值,

  • base – 0
  • limit – 0xfffff
  • flags – 0xc09b

段基址为0 最大值为0xfffff 标志位

1100 0000 1001 1011

二进制数据每一位都有相应含义

从左到右依次

  • 1 – (G)粒度标志
  • 1 – (D) 0 16位段; 1 = 32位段
  • 0 – (L) 如果是1 则在64位下
  • 0 – (AVL) 允许使用系统软件
  • 0000 – 4bit 长 在描述符里19:16bits
  • 1 – (P) 段在内存中
  • 00 – (DPL) – 权限等级
  • 1 – (S) 代码段或者数据段,非系统段
  • 101 – 段类型 可执行?/可读?
  • 1 – accessed bit

获取GDT长度大小

gdt.len = sizeof(boot_gdt)-1;

获取指向GDT的指针

gdt.ptr = (u32)&boot_gdt + (ds() << 4);

这里将ds()左移4位,(在实模式下)

最后执行lgdtl加载GDT到GDTR寄存器

asm volatile("lgdtl %0" : : "m" (gdt));

转到保护模式

go_to_protected_mode函数的最后,加载了IDT和GDT,禁用中断,可以将cpu转入保护模式.最后一步通过 protected_mode_jump 完成,

protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));

参数1 : 保护模式进入的地址

参数2 : boot_params地址

第一个参数被放在eax里第二个在edx里,

首先将boot_params地址放在esi寄存器里,代码段的cs寄存器放入bx

GLOBAL(protected_mode_jump)
    movl    %edx, %esi    # Pointer to boot_params table

    xorl    %ebx, %ebx
    movw    %cs, %bx

之后,bx左移4位,加上标签2处的值((cs << 4) + in_pm32)

跳转到标签1

    shll    $4, %ebx
    addl    %ebx, 2f     # Add %ebx to the value stored at label 2
    jmp    1f        # Short jump to serialize on 386/486

之后lable2处值会被写为(cs << 4) + in_pm32

数据段和TSS段分别放在cx di寄存器内.

    movw    $__BOOT_DS, %cx
    movw    $__BOOT_TSS, %di

PE放入控制寄存器

    movl    %cr0, %edx
    orb    $X86_CR0_PE, %dl
    movl    %edx, %cr0

长跳转到保护模式

    .byte    0x66, 0xea
2:    .long    in_pm32
    .word    __BOOT_CS

0x66是允许操作数大小 混合16bit和32bit代码

0xea 跳转的机器码

in_pm32是保护模式下段偏移

__BOOT_CS 我们跳转到的代码段

最终进入保护模式

.code32
.section ".text32","ax"

进入保护模式后,设置数据段

movl    %ecx, %ds
movl    %ecx, %es
movl    %ecx, %fs
movl    %ecx, %gs
movl    %ecx, %ss

我们曾将cs寄存器设置为$__BOOT_DS,现在将所有的寄存器设置(除cs外)

为debug需求,设置有效的栈

清空所有的寄存器

xorl    %ecx, %ecx
xorl    %edx, %edx
xorl    %ebx, %ebx
xorl    %ebp, %ebp
xorl    %edi, %edi

跳转到32位入口点

jmpl    *%eax

eax在protected_mode_jump时已经设置
到此完成转换

Links

(完)