这个实验主要包含三部分
第一部分是定义全局描述符表gdt,以及全局描述符表指针gdtr
第二部分是16位代码段,主要工作有:
- 关中断
- 开启A20
- 开启保护模式
- 跳到32位代码段
第三部分是32位代码段,往显存里写一个字母’p’
分析
首先第一部分是纯数据,可以用C语言来实现,代码如下:
desc.h
#include <stdint.h>
// Type field
#define TYPE_DATA_RO 0
#define TYPE_DATA_RO_A 1
#define TYPE_DATA_RW 2
#define TYPE_DATA_RW_A 3
#define TYPE_DATA_RO_E 4
#define TYPE_DATA_RO_E_A 5
#define TYPE_DATA_RW_E 6
#define TYPE_DATA_RW_E_A 7
#define TYPE_CODE_XO 8
#define TYPE_CODE_XO_A 9
#define TYPE_CODE_XR 10
#define TYPE_CODE_XR_A 11
#define TYPE_CODE_XO_C 12
#define TYPE_CODE_XO_C_A 13
#define TYPE_CODE_XR_C 14
#define TYPE_CODE_XR_C_A 15
#define TYPE_SYS_TSS_16_A 1
#define TYPE_SYS_LDT 2
#define TYPE_SYS_TSS_16_B 3
#define TYPE_SYS_CALL_GATE_16 4
#define TYPE_SYS_TASK_GATE 5
#define TYPE_SYS_INT_GATE_16 6
#define TYPE_SYS_TRAP_GATE_16 7
#define TYPE_SYS_TSS_32_A 9
#define TYPE_SYS_TSS_32_B 11
#define TYPE_SYS_CALL_GATE_32 12
#define TYPE_SYS_INT_GATE_32 14
#define TYPE_SYS_TRAP_GATE_32 15
// S(descripor type) flag
#define DESC_TYPE_SYSTEM 0
#define DESC_TYPE_CODE 1
#define DESC_TYPE_DATA 1
// DPL(descriptor privilege level) field
#define DPL_0 0
#define DPL_1 1
#define DPL_2 2
#define DPL_3 3
// P(segment-present) flag
#define SEG_PRESENT 1
#define SEG_NOT_PRESENT 0
// D/B flag
#define CODE_32 1
#define CODE_16 0
#define DATA_32 1
#define DATA_16 0
// G(granularity) flag
#define G_BYTE 0
#define G_4K_BYTE 1
typedef struct __attribute__((packed)) {
uint16_t limit;
uint32_t base;
} gdtr_t;
typedef struct {
uint64_t limit_15_0:16;
uint64_t base_23_0:24;
uint64_t type:4;
uint64_t s:1;
uint64_t dpl:2;
uint64_t p:1;
uint64_t limit_19_16:4;
uint64_t avl:1;
uint64_t res:1;
uint64_t db:1;
uint64_t g:1;
uint64_t base_31_24:8;
} seg_desc_t;
#define SEG_DESC(_base, _limit, _type, _s, _dpl, _p, _db, _g) { \
.base_23_0 = (_base) & 0xffffff, \
.base_31_24 = ((_base) >> 24) & 0xff, \
.limit_15_0 = (_limit) & 0xffff, \
.limit_19_16 = ((_limit) >> 16) & 0xf, \
.type = (_type), \
.s = (_s), \
.dpl = (_dpl), \
.p = (_p), \
.avl = 0, \
.res = 0, \
.db = (_db), \
.g = (_g), \
}
上述代码定义了两个结构,分别是:
gdtr_t
表示全局描述符指针seg_desc_t
表示段描述符
boot1.c
#include "desc.h"
seg_desc_t gdt[3] = {
[0] = SEG_DESC(0, 0, 0, 0, 0, 0, 0, 0),
[1] = SEG_DESC(0x7c80, 0xff, TYPE_CODE_XO, DESC_TYPE_CODE, DPL_0, SEG_PRESENT, CODE_32, G_BYTE),
[2] = SEG_DESC(0xb8000, 0xffff, TYPE_DATA_RW, DESC_TYPE_DATA, DPL_0, SEG_PRESENT, DATA_32, G_BYTE),
};
gdtr_t gdtr = {
.limit = sizeof(gdt)-1,
.base = (uint32_t)&gdt,
};
上述代码定义了两个对象:
全局描述符表gdt
按要求全局描述符表的第一描述符是空描述符
第二个描述符是代码段描述符,基地址是0x7c80,段大小(limit+1)是256字节,在这里够用了
第三个描述符是数据段描述符,基地址是0xb8000,这个对应的是显存地址,段大小(limit+1)是64KB全局描述符表指针gdtr
这个指针有两个字段,base指向全局描述符表,limit指定全局描述符表的界限
ok,第一部分数据定义完成了,下面看16位代码段和32位代码段
16位代码段以及32位代码
#define CODE_SELECTOR 0x8
#define DATA_SELECTOR 0x10
#define CURSOR_POS ((80 * 11 + 79) * 2)
.code16
.section text16
.globl _start
_start:
cli
# load gdtr
lgdt gdtr
# enable a20
inb $0x92, %al
orb $0b10, %al
outb %al, $0x92
# enable pm
movl %cr0, %eax
orl $0x01, %eax
movl %eax, %cr0
# jump to pm
jmpl $CODE_SELECTOR, $0x0
hlt
.code32
.section text32
movw $DATA_SELECTOR, %ax
movw %ax, %gs
movl $CURSOR_POS, %edi
movb $0x0c, %ah
movb $0x70, %al
movw %ax, %gs:(%edi)
jmp .
在16位代码段中加载gdtr只需要用
lgdt gdtr
指令就可以,这个gdtr引用的就是在boot1.c中定义的gdtr。
跳转到32位代码段的指令jmpl $CODE_SELECTOR, $0x0
,其中CODE_SELECTOR为8,就是1<<3,就是引用全局描述符表中的第1个描述符,即,代码段。
32位代码段中将指向第2个描述符(就是数据段描述符)的选择子存到gs寄存器中。
程序链接
先写一个链接脚本,如下:
OUTPUT_FORMAT(binary)
ENTRY(_start)
MEMORY {
boot_region (RX) : org = 0x7c00, len = 512
}
SECTIONS {
.text : {
boot0.o(text16)
. = 64;
boot1.o(.data*)
. = 128;
boot0.o(text32)
. = 510;
SHORT(0xaa55)
} >boot_region
/DISCARD/ : {
*(*)
}
}
首先定义一个内存区,这个内存区是可读(R)可执行(X)的,起始地址为0x7c00,大小为512字节
注意: 32位代码段放到了0x7c00+128字节处,也就是0x7c80处,这就是为什么全局描述符表中的代码段描述符的基地址为0x7c80的原因
Makefile
a.img: boot.bin
-rm a.img
bximage -q -mode=create -fd=1.44M a.img
dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
boot.bin: boot0.o boot1.o boot.lds
ld -Tboot.lds boot0.o boot1.o -o boot.bin
ld -Tboot.lds boot0.o boot1.o --oformat=elf32-i386 -o boot.elf # for objdump
boot0.o: boot0.S
gcc -c boot0.S -o boot0.o
boot1.o: boot1.c desc.h
gcc -c boot1.c -o boot1.o
.PHONY: clean
clean:
-rm boot0.o boot1.o boot.bin boot.elf a.img
这里链接了两次,分别输出了boot.bin和boot.elf,输出boot.elf是为了使用
objdump
工具查看反汇编的结果
下面是 objdump -dj .text boot.elf
的内容:
boot.elf: file format elf32-i386
Disassembly of section .text:
00007c00 <_start>:
7c00: fa cli
7c01: 0f 01 16 lgdtl (%esi)
7c04: 58 pop %eax
7c05: 7c e4 jl 7beb <_start-0x15>
7c07: 92 xchg %eax,%edx
7c08: 0c 02 or $0x2,%al
7c0a: e6 92 out %al,$0x92
7c0c: 0f 20 c0 mov %cr0,%eax
7c0f: 66 83 c8 01 or $0x1,%ax
7c13: 0f 22 c0 mov %eax,%cr0
7c16: 66 ea 00 00 00 00 ljmpw $0x0,$0x0
7c1c: 08 00 or %al,(%eax)
7c1e: f4 hlt
...
00007c40 <gdt>:
...
7c48: ff 00 80 7c 00 98 40 00 ff ff 00 80 0b 92 40 00 ...|..@.......@.
00007c58 <gdtr>:
7c58: 17 00 40 7c 00 00 00 00 00 00 00 00 00 00 00 00 ..@|............
...
7c80: 66 b8 10 00 8e e8 bf 7e 07 00 00 b4 0c b0 70 65 f......~......pe
7c90: 66 89 07 eb fe 00 00 00 00 00 00 00 00 00 00 00 f...............
...
7dfc: 00 00 55 aa ..U.
可以看到:
16位代码段的起始地址为0x7c00
存放gdt和gdtr两个对象的数据段的起始地址为0x7c40
32位代码段的起始地址为0x7c80
运行效果
可以看到屏幕中央右侧有一个红色的字母
p