从零编写linux0.11 - 第二章 内核初始化

编程环境:Ubuntu Kylin 16.04

代码仓库:https://gitee.com/AprilSloan/linux0.11-project

linux0.11源码下载(不能直接编译,需进行修改)

1.添加内核

进入内核后也不是马上进入main函数,上一章只是暂时设置gdt,此外还要设置idt和其他的一些初始化操作,这些内容也要使用汇编进行编写,此处采用AT&T格式的汇编。

以下为head.s的内容,为了简单起见,内容还是一个死循环。

.globl startup_32
startup_32:
    jmp startup_32

第一行是为了让外部文件能使用startup_32。内核不仅仅需要编译,还需要链接,这里需要写一个链接控制脚本,命名为kernel.lds。

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(startup_32)
SECTIONS
{
	.text :
	{
		_text = .;
		*(.text)
		_etext = .;
	}
	. = ALIGN(8);
	.data :
	{
		_data = .;
		*(.data)
		_edata = .;
	}
	.bss :
	{
		_bss = .;
		*(.bss)
		_ebss = .;
	}
	_end = .;
}

这里定义了输出的格式和架构都是386的,函数入口指定为startup_32,将各个文件的text段、data段和bss段合并,_end为内核的结尾地址,以后会用到。

同时,需要在Makefile里添加head.s的编译规则。

LD	=ld
LDFLAGS	=-m elf_i386

default: all

all: Image

Image: mkimg boot/bootsect.bin boot/setup.bin system
	objcopy -O binary -R .note -R .comment system kernel
	dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
	dd if=boot/setup.bin of=kernel.img bs=512 count=4 seek=1 conv=notrunc
	dd if=kernel of=kernel.img bs=512 count=1 seek=5 conv=notrunc
	rm kernel -f
	bochs -qf bochsrc

boot/head.o: boot/head.s
	gcc -m32 -traditional -c boot/head.s -o boot/head.o

system:	boot/head.o
	$(LD) $(LDFLAGS) boot/head.o -o system -T kernel.lds
	nm system | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aU] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)'| sort > System.map 

boot/bootsect.bin: boot/bootsect.s
	nasm boot/bootsect.s -o boot/bootsect.bin

boot/setup.bin: boot/setup.s
	nasm boot/setup.s -o boot/setup.bin

mkimg:
	./mkimg.sh

clean:
	rm -rf boot/*.bin boot/*.o System.map system kernel.img

这里使用gcc编译head.s,之后通过链接脚本生成一个名为system的文件,也生成了System.map,这个文件中记录了内核所有函数和全局变量的地址,在调试时十分有用。去除system中的注释信息生成kernel,依次将bootsect.bin,setup.bin,kernel写入软盘中。为了方便,假设内核部分只占1个扇区,之后的小节再进行完善。

head.s写好了还没完,毕竟bootsect.s并没有将head.s加载进内存的内容,下面来添加这部分内容。

SETUPLEN    equ 4
BOOTSEG     equ 0x07c0
INITSEG     equ 0x9000
SETUPSEG    equ 0x9020
SYSSEG      equ 0x1000

start:
    mov ax, BOOTSEG
    mov ds, ax
    mov ax, INITSEG
    mov es, ax
    mov cx, 256
    sub si, si
    sub di, di
    rep
    movsw           ; 将bootsect.s从0x7c00移动到0x90000
    jmp INITSEG:go
go: mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0xff00

load_setup:
    mov dx, 0x00
    mov cx, 0x02
    mov bx, 0x0200
    mov ax, 0x0200 + SETUPLEN
    int 0x13        ; 加载setup.s到0x90200
    jnc ok_load_setup
    mov dx, 0x00
    mov ax, 0x00
    int 0x13
    jmp load_setup  ; 加载失败则复位软盘,重新加载

ok_load_setup:
    mov ah, 0x03
    xor bh, bh
    int 0x10        ; 获取光标位置

    mov cx, 24
    mov bx, 0x0007
    mov bp, msg
    mov ax, 0x1301
    int 0x10        ; 打印字符串

load_system:
    mov ax, SYSSEG
    mov es, ax
    mov dx, 0x00
    mov cx, 0x06
    mov bx, 0x00
    mov ax, 0x0201
    int 0x13        ; 加载head.s到0x10000
    jnc ok_load_system
    mov dx, 0x00
    mov ax, 0x00
    int 0x13
    jmp load_system ; 加载失败则复位软盘,重新加载

ok_load_system:
    jmp SETUPSEG:0  ; 跳转到setup.s的内容

msg:
    db 13, 10
    db "Loading system ..."
    db 13, 10, 13, 10

    times 0x1fe - ($ - $$) db 0 ; 填写0,直到0x1fe
    dw 0xaa55       ; 启动盘标识

这次修改添加了load_system这一块代码,其代码与load_setup相似。

这就可以调试了吗?不不不,上一章设置gdt时,代码段的起始地址设置为0,而现在内核地址为0x10000,我们还需要把内核从0x10000移动到0地址,这部分功能在setup.s中完成。

INITSEG     equ 0x9000
SYSSEG      equ 0x1000
SETUPSEG    equ 0x9020

start:
    cli     	; 保护模式下中断机制尚未建立,应禁止中断
    mov ax, 0x00
    cld
do_move:
    mov es, ax
    mov ax, SYSSEG
    mov ds, ax
    sub di, di
    sub si, si
    mov cx, 256
    rep
    movsw       ; 将一个扇区从0x10000移动到0x00

end_move:
    mov ax, SETUPSEG
    mov ds, ax
    lgdt    [gdt_48]    ; 加载gdtr

    mov al, 2
    out 0x92, al        ; 开A20

    mov ax, 0x0001
    lmsw    ax          ; 设置CR0的PE位
    jmp 8:0

gdt:
    dw  0, 0, 0, 0
    dw  0x07ff, 0x0000, 0x9a00, 0x00c0
    dw  0x07ff, 0x0000, 0x9200, 0x00c0

gdt_48:
    dw  0x800
    dw  512 + gdt, 0x9  ; 0x90200+gdt

移动内核的代码与bootsect.s移动自己的代码相似。另外,把死循环改成跳转到内核中,这里需要着重讲解一下jmp 8:0的意思。jmp 8:0与之前bootsect.s中jmp SETUPSEG:0的意思并不一样,要注意,我们已经开启了保护模式,此处的8代表了段选择符,请看下图。

段选择符

TI=0表示在GDT(全局描述符表)中,1表示在LDT(局部描述符表)中,RPL表示优先级,0最高。所以8代表了索引为1的段描述符(索引为0的段描述符为空),此段描述符优先级为0,在GDT中。该段描述符就是setup.s第32行设置的代码段。

好的,现在就可以开始调试了。你可以一步一步运行,看看运行流程,也可以像我一样在0地址打断点,直接跳转到这里。

地址0

可以看到head.s中的死循环,运行成功!

下一节会进一步完善内核的加载与移动,毕竟内核的代码一个扇区可装不完。

2.完善内核加载与移动

内核移动情况

通过上图可知,bootsect.s将内核加载到0x10000开始的地址,setup.s将内核移动到0开始的地址。Linus认为对于linux0.11版本,内核最多也就192KB(0x30000,384个扇区),所以程序中也就只加载了这些扇区到内存中。

将bootsect.s修改后:

SYSSIZE     equ 0x3000
SETUPLEN    equ 4
BOOTSEG     equ 0x07c0
INITSEG     equ 0x9000
SETUPSEG    equ 0x9020
SYSSEG      equ 0x1000
ENDSEG      equ SYSSEG + SYSSIZE

start:
    mov ax, BOOTSEG
    mov ds, ax
    mov ax, INITSEG
    mov es, ax
    mov cx, 256
    sub si, si
    sub di, di
    rep
    movsw           ; 将bootsect.s从0x7c00移动到0x90000
    jmp INITSEG:go
go: mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0xff00

load_setup:
    mov dx, 0x00
    mov cx, 0x02
    mov bx, 0x0200
    mov ax, 0x0200 + SETUPLEN
    int 0x13        ; 加载setup.s到0x90200
    jnc ok_load_setup
    mov dx, 0x00
    mov ax, 0x00
    int 0x13
    jmp load_setup  ; 加载失败则复位软盘,重新加载

ok_load_setup:
    mov dl, 0x00
    mov ax, 0x0800
    int 0x13        ; 获取每磁道扇区数
    mov ch, 0x00
    mov [sectors], cx
    mov ax, INITSEG
    mov es, ax

    mov ah, 0x03
    xor bh, bh
    int 0x10        ; 获取光标位置

    mov cx, 24
    mov bx, 0x0007
    mov bp, msg
    mov ax, 0x1301
    int 0x10        ; 打印字符串

    mov ax, SYSSEG
    mov es, ax
    call    read_it
    call    kill_motor

    jmp SETUPSEG:0  ; 跳转到setup.s的内容

read_it:
    mov ax, es
    test    ax, 0x0fff
die:jne     die
    xor bx, bx
rp_read:
    mov ax, es
    cmp ax, ENDSEG
    jb  ok1_read
    ret
ok1_read:
    mov ax, [sectors]
    sub ax, [sread]
    mov cx, ax
    shl cx, 9
    add cx, bx
    jnc ok2_read
    je  ok2_read
    xor ax, ax
    sub ax, bx
    shr ax, 9
ok2_read:
    call    read_track
    mov cx, ax
    add ax, [sread]
    cmp ax, [sectors]
    jne ok3_read
    mov ax, 1
    sub ax, [head]
    jne ok4_read
    push ax
    mov ax, [track]
    inc ax
    mov [track], ax
    pop ax
ok4_read:
    mov [head], ax
    xor ax, ax
ok3_read:
    mov [sread], ax
    shl cx, 9
    add bx, cx
    jnc rp_read
    mov ax, es
    add ax, 0x1000
    mov es, ax
    xor bx, bx
    jmp rp_read

read_track:
    push    ax
    push    bx
    push    cx
    push    dx
    mov dx, [track]
    mov cx, [sread]
    inc cx
    mov ch, dl
    mov dx, [head]
    mov dh, dl
    mov dl, 0x00
    and dx, 0x0100
    mov ah, 2
    int 0x13
    jc  bad_rt
    pop dx
    pop cx
    pop bx
    pop ax
    ret
bad_rt:
    mov ax, 0
    mov dx, 0x00
    int 0x13
    pop dx
    pop cx
    pop bx
    pop ax
    jmp read_track

kill_motor:
    push    dx
    mov dx, 0x3f2
    mov al, 0
    out dx, al
    pop dx
    ret

sread:
    dw  1 + SETUPLEN
head:
    dw  0
track:
    dw  0
sectors:
    dw  0

msg:
    db 13, 10
    db "Loading system ..."
    db 13, 10, 13, 10

    times 0x1fe - ($ - $$) db 0 ; 填写0,直到0x1fe
    dw 0xaa55       ; 启动盘标识

第39-43行是为了获取每磁道扇区数,并保存下来。

这段代码中最难以理解的就是read_itkill_motor之间用于读取扇区的代码,我将这段代码写成C语言伪代码形式帮助大家理解。

#define SYSSIZE 0x3000
#define SETUPLEN 4
#define SYSSEG 0x1000
#define ENDSEG SYSSEG + SYSSIZE	// 0x4000

short ax = SYSSEG, bx, cx, dx, es = SYSSEG;
short sread = 1 + SETUPLEN, head = 0, track = 0, sectors = 18;

void read_it(void) {
	if (ax & 0xfff)
		while (1);			// die
	bx = 0;
	while (1) {				// rp_read
		ax = es;
		if (ax >= ENDSEG)
			return;
		ax = sectors;		// ok1_read
		ax -= sread;		// 在当前磁道、磁头下,要读取的扇区数
		cx = ax;
		cx *= 512;			// 要读取的字节数
		cx += bx;			// 读取完成后总读取字节数
		if (cx溢出 && cx != 0) {
			ax = 0x10000 - bx;
			ax /= 512;
		}
		read_track();		// ok2_read
		cx = ax;
		ax += sread;		// 在当前磁道、磁头下,已读取的扇区数
		if (ax = sectors) {	// 如果读取完当前磁道、磁头的所有扇区
			if (head == 1) {
				track++;	// 更改磁道
			}
			head = !head;	// ok4_read	更改磁头
			ax = 0;
		}
		sread = ax;			// ok3_read
		cx *= 512;
		bx += cx;			// 更新数据存放地址
		if (bx未溢出)
			continue;
		es += 0x1000;
		bx = 0;				// 读取完64KB数据,更新数据存放地址
	}
}

void read_track(void) {
	while (1) {				// read_track
    	push ax,bx,cx,dx
		cx = track;
		cx <<= 8;
		cx += sread + 1;
		dx = head;
		dx <<= 8;
		dx &= 0x0100;
		ax &= 0x00ff;
		ax |= 0x0200;
		int 0x13;
        if (读取成功) {
            pop ax,bx,cx,dx
            return;
        }
		else {				// bad_rt
			ax = 0;
			dx = 0;
			int 0x13;
            pop ax,bx,cx,dx
		}
	}
}

结合c语言伪代码和汇编代码,相信大家更容易理解。这里的读取扇区操作有些复杂,既然我已经知道要读取的扇区数,知道要存放的地址,能不能一次性全部读取出来呢?答案是不可以。让我们重新回顾一下读取扇区的BIOS中断知识。

int_0x13_ah_2

我们每次读取扇区需要指定磁道和磁头,而我们创建的软盘的每条磁道有18个扇区,所以每次最多也就读取18个扇区。而且还得注意扇区号,扇区号是由track,head,sector一起构成的,扇区号从1开始,内核保存在6号扇区到389号扇区内。

  • 1号扇区 => track=0,head=0,sector=1
  • 2号扇区 => track=0,head=0,sector=2
  • 19号扇区=> track=0,head=1,sector=1
  • 37号扇区=> track=1,head=0,sector=1

所以每次调用read_track后都要更新head和track的值。另外,读取的扇区数不一定是18个,比如第一次只读取6-18号扇区。

kill_motor之后的代码就没什么解说的必要了。我们已经读取了软盘所有的数据,此时软盘就没用了,将它的电动机关掉。

接下来还需要修改setup.s的内容。

INITSEG     equ 0x9000
SYSSEG      equ 0x1000
SETUPSEG    equ 0x9020

start:
	cli         		; 保护模式下中断机制尚未建立,应禁止中断
    mov ax, 0x00
    cld
do_move:                ; 将内核从0x10000移动到0x00
    mov es, ax
    add ax, 0x1000
    cmp ax, 0x9000
    jz  end_move
    mov ds, ax
    sub di, di
    sub si, si
    mov cx, 0x8000
    rep
    movsw               ; 每次移动64KB内容
    jmp do_move

end_move:
    mov ax, SETUPSEG
    mov ds, ax
    lgdt    [gdt_48]    ; 加载gdtr

    mov al, 2
    out 0x92, al        ; 开A20

    mov ax, 0x0001
    lmsw    ax          ; 设置CR0的PE位
    jmp 8:0

gdt:
    dw  0, 0, 0, 0
    dw  0x07ff, 0x0000, 0x9a00, 0x00c0
    dw  0x07ff, 0x0000, 0x9200, 0x00c0

gdt_48:
    dw  0x800
    dw  512 + gdt, 0x9  ; 0x90200+gdt

相比上一节改动的地方不多,只修改了do_move下的内容。现在,程序会移动0x10000-0x90000的数据到0-0x80000地址,每次移动64KB数据。

这一节head.s就不做改动了,毕竟改动一些内容也玩不出花儿来,在下一节中添加东西吧。

最后小小的修改一下Makefile。

Image: mkimg boot/bootsect.bin boot/setup.bin system
	objcopy -O binary -R .note -R .comment system kernel
	dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
	dd if=boot/setup.bin of=kernel.img bs=512 count=4 seek=1 conv=notrunc
	dd if=kernel of=kernel.img bs=512 count=384 seek=5 conv=notrunc
	rm kernel -f
	bochs -qf bochsrc

只对第5行的内容进行了修改,更改了写入内核的数据量,不然即使修改了bootsect.s,加载进内存的也只有一个扇区的数据。

运行一下看有没有错误。(如果你直接跳转到0地址,需要等待一段时间,因为软盘加载扇区速度比较慢)

2.2运行结果

反正肯定是没错的,不然我也不会写出来了,这就是为了水篇幅。

3.开始编写内核

进入内核后要做不少初始化操作,而现在,需要重新加载gdt并设置idt,于是需要对head.s进行如下改动:

.text
.globl idt, gdt
.globl startup_32
startup_32:
    movl    $0x10, %eax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    lss stack_start, %esp
    call setup_idt
    call setup_gdt
    movl    $0x10, %eax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    lss stack_start, %esp
    jmp .

setup_idt:
    lidt    idt_descr
    ret

setup_gdt:
    lgdt    gdt_descr
    ret

.align  4
.word   0
idt_descr:
	.word   256 * 8 - 1
	.long   idt

.align  4
.word   0
gdt_descr:
	.word   256 * 8 - 1
	.long   gdt

.align  8
idt:.fill   256, 8, 0

gdt:.quad   0x0000000000000000
	.quad   0x00c09a0000000fff
	.quad   0x00c0920000000fff
	.quad   0x0000000000000000
	.fill   252, 8, 0

进入保护模式,首先设置段寄存器,并设置栈段寄存器和栈指针寄存器,让栈指针寄存器指向用户栈数组末尾。其中stack_start定义在main.c中。

接着是设置idt和gdt,idt是中断描述符表,用于指定异常或中断服务函数的地址,gdt在之前已经讲过不再赘述。这两个函数都很简单。idt和gdt都设置有256个表项,现在仅仅把idt的内容都设置为0,而gdt的内容与setup.s中的设置相似,只是更改了段界限。

设置完idt和gdt后,需要重新设置各个段寄存器和栈相关寄存器,最后进入死循环(我也找不到其他的结束方式了)。

这一节中新添加了一个文件:main.c。我将用户栈的数据保存在这个文件中,在之后的章节中会在这个文件中写main函数。

#define PAGE_SIZE   4096

long user_stack[PAGE_SIZE >> 2];

struct {
	long *a;
	short b;
} stack_start = {&user_stack[PAGE_SIZE >> 2], 0x10};

这里定义了一页大小为4096,用户栈大小为4096字节。

因为添加了新文件,需要更改Makefile,添加main.c的编译规则,并将main.o用于其他文件的编译规则内,当然也不要忘了在clean下添加它。

AS	=as --32
LD	=ld
LDFLAGS	=-m elf_i386
CC	=gcc -march=i386
CFLAGS	=-Wall -O2 -m32 -fomit-frame-pointer -fno-stack-protector

default: all

all: Image

Image: mkimg boot/bootsect.bin boot/setup.bin system
	objcopy -O binary -R .note -R .comment system kernel
	dd if=boot/bootsect.bin of=kernel.img bs=512 count=1 conv=notrunc
	dd if=boot/setup.bin of=kernel.img bs=512 count=4 seek=1 conv=notrunc
	dd if=kernel of=kernel.img bs=512 count=384 seek=5 conv=notrunc
	rm kernel -f
	bochs -qf bochsrc

boot/head.o: boot/head.s
	gcc -m32 -traditional -c boot/head.s -o boot/head.o

system:	boot/head.o init/main.o
	$(LD) $(LDFLAGS) boot/head.o init/main.o \
	-o system -T kernel.lds
	nm system | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aU] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)'| sort > System.map 

boot/bootsect.bin: boot/bootsect.s
	nasm boot/bootsect.s -o boot/bootsect.bin

boot/setup.bin: boot/setup.s
	nasm boot/setup.s -o boot/setup.bin

mkimg:
	./mkimg.sh

clean:
	rm -rf boot/*.bin boot/*.o init/*.o System.map system kernel.img

init/main.o: init/main.c
	$(CC) $(CFLAGS) -nostdinc -Iinclude -c -o $*.o $<

运行一下吧。

2.3运行结果

最后运行到了死循环,表示这是没有问题的。大家可以看看esp,ss,gdtr,idtr等寄存器的值是否与预想的一致。这一节内容较少,并不难理解,让我们赶快开始下一节的内容吧。

4.设置页表

这节会设置页目录和页表。将0x0-0x1000分配给页目录,0x1000-0x2000分配给页表0,0x2000-0x3000分配给页表1,0x3000-0x4000分配给页表2,0x4000-0x5000分配给页表3。所有页表都要记录到页目录中,页要记录到页表中,一个页目录项记录一张页表的地址,占4字节,一个页表项记录一页的地址,占4字节,页大小为4KB,所以每个页表可以记录4MB内存的使用情况,每个页目录可以记录4GB内存使用情况。这里,我们只使用4个页表,只寻址16MB内存。

你可能会问,0x0地址不是有代码吗,怎么可以给页目录?确实这个地址有代码,但是当代码执行完就可以覆盖掉,相当于卸磨杀驴。

修改后的head.s的内容如下:

.text
.globl idt, gdt, pg_dir, tmp_floppy_area
pg_dir:
.globl startup_32
startup_32:
    movl    $0x10, %eax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    lss stack_start, %esp
    call setup_idt
    call setup_gdt
    movl    $0x10, %eax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %fs
    mov %ax, %gs
    lss stack_start, %esp
    xorl    %eax, %eax
1:  incl    %eax        # 检查是否开启A20
    movl    %eax, 0x0
    cmpl    %eax, 0x100000
    je 1b

    jmp after_page_tables

setup_idt:
    lidt    idt_descr
    ret

setup_gdt:
    lgdt    gdt_descr
    ret

.org    0x1000
pg0:

.org    0x2000
pg1:

.org    0x3000
pg2:

.org    0x4000
pg3:

.org    0x5000

tmp_floppy_area:
    .fill   1024, 1, 0

after_page_tables:
    pushl   $0
    pushl   $0
    pushl   $0
    pushl   $L6     # 设置main函数的返回地址
    pushl   $main   # 设置setup_paging的返回地址为main函数
    jmp setup_paging
L6: jmp L6

.align  4
setup_paging:
    movl    1024 * 5, %ecx
    xorl    %eax, %eax
    xorl    %edi, %edi
    cld
    rep
    stosl
    movl    $pg0 + 7, pg_dir
    movl    $pg1 + 7, pg_dir + 4
    movl    $pg2 + 7, pg_dir + 8
    movl    $pg3 + 7, pg_dir + 12   # 在页目录中设置页表信息
    movl    $pg0, %edi
    movl    $0x0007, %eax
1:  stosl
    addl    $0x1000, %eax
    cmpl    $0x1000007, %eax
    jne 1b                  # 设置页信息
    xorl    %eax, %eax
    movl    %eax, %cr3
    movl    %cr0, %eax
    orl $0x80000000, %eax
    movl    %eax, %cr0      # 设置CR0的PG位,开启分页
    ret

.align  4
.word   0
idt_descr:
	.word   256 * 8 - 1
	.long   idt

.align  4
.word   0
gdt_descr:
	.word   256 * 8 - 1
	.long   gdt

.align  8
idt:.fill   256, 8, 0

gdt:.quad   0x0000000000000000
	.quad   0x00c09a0000000fff
	.quad   0x00c0920000000fff
	.quad   0x0000000000000000
	.fill   252, 8, 0

首先在head.s开始的地方设置了一个pg_dir标签,这是用来指明页目录的地址。然后是设置段寄存器、栈、idt和gdt,接着检查A20是否开启,如果A20未开启,0地址的内容就会和0x100000(1MB)地址的内容相同,这里请注意AT&T汇编内存寻址是直接用数字,如0x100000,而Intel汇编内存寻址是用中括号括上数字,如[0x100000]。第24行的je 1b的意思是如果ZF=1,则跳转到上一个名字为1的标签,也就是第21行的1标签。

第36-46行设置了四张页表的地址。.org 0x1000的意思是此语句后的程序或数据以0x1000为起始地址,所以pg0标签的地址为0x1000,同理,pg1标签的地址为0x2000,pg2标签的地址为0x3000,pg3标签的地址为0x4000。

tmp_floppy_area定义了一个给软盘驱动程序使用的内存块,在DMA不能访问缓冲块时使用。

after_page_tables的前三行设置了main函数的参数,pushl $L6设置了main函数的返回地址(L6是一个死循环),pushl $main将main函数地址推入栈中。这一段要与setup_paging的ret指令一起理解,ret的作用是取出栈顶数值放入eip中,setup_paging中没有入栈出栈操作,所以当ret执行时会把main函数地址放入eip中,此时程序从main函数开始继续执行。很巧妙的操作。

cld rep stosl这三条指令的意思是:把eax的数据装入es:edi指向的地址,递增edi,递减ecx直至ecx为0。所以第64-69行代码的目的是把0x0-0x5000的内容填充为0,即把页目录和页表都清零。

第70-73行在页目录中设置页表信息。页目录表项的结构如下所示:

页目录表项

字段意义
P存在位。为1表示页表或者页位于内存中。否则,表示不在内存中
R/W读写标志。为1表示页面可以被读写,为0表示只读。当处理器运行在0、1、2特权级时,此位不起作用。
U/S用户/超级用户标志。为1时,允许所有特权级别的程序访问;为0时,仅允许特权级为0、1、2的程序访问。
PWTPage级的Write-Through标志位。为1时使用Write-Through的Cache类型;为0时使用Write-Back的Cache类型。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。
PCDPage级的Cache Disable标志位。为1时,物理页面是不能被Cache的;为0时允许Cache。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。
A访问位。该位由处理器固件设置,用来指示此表项所指向的页是否已被访问(读或写),一旦置位,处理器从不清这个标志位。这个位可以被操作系统用来监视页的使用频率。
PSPage Size位。为0时,页的大小是4KB;为1时,页的大小是4MB(普通32位地址寻址)或者2MB(如果开启扩展物理地址寻址).
G全局位。如果页是全局的,那么它将在高速缓存中一直保存。
Avail.被处理器忽略,软件可以使用。
Page-Table Base Address页表基地址

我们以movl $pg0 + 7, pg_dir为例进行说明。pg0的地址为0x1000,这条语句是把第1个页目录表项设置为0x1007,此时页表0的基地址为1(4KB对齐),页大小为4KB,能被所有特权级别的程序访问,可读可写,已存在内存中。对于页表1,页表2,页表3我们也能用相同方式理解。

第74-79行用于设置页表项。页表项结构如下图所示:

页表项结构

字段意义
P存在位。为1表示页表或者页位于内存中。否则,表示不在内存中
R/W读写标志。为1表示页面可以被读写,为0表示只读。当处理器运行在0、1、2特权级时,此位不起作用。
U/S用户/超级用户标志。为1时,允许所有特权级别的程序访问;为0时,仅允许特权级为0、1、2的程序访问。
PWTPage级的Write-Through标志位。为1时使用Write-Through的Cache类型;为0时使用Write-Back的Cache类型。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。
PCDPage级的Cache Disable标志位。为1时,物理页面是不能被Cache的;为0时允许Cache。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。
A访问位。该位由处理器固件设置,用来指示此表项所指向的页是否已被访问(读或写),一旦置位,处理器从不清这个标志位。这个位可以被操作系统用来监视页的使用频率。
D脏位。该位由处理器固件设置,用来指示此表项所指向的页是否写过数据。
G全局位。如果页是全局的,那么它将在高速缓存中一直保存。
Avail.被处理器忽略,软件可以使用。
Page-Table Base Address页基地址

以edi=pg0,eax=0x7为例,stosl指令会将eax的数值写入es:edi内存地址中,即页表0的第1个页表项被设置为:页基地址为0,允许所有特权级别的程序访问,可读可写,页位于内存中。之后edi指向页表0的第2个页表项,eax=0x1007,设置第二个页表项为:页基地址为1,允许所有特权级别的程序访问,可读可写,页位于内存中。不断重复,直至将4张页表的所有页表项都设置完。

我们在页表中设置了页的信息,在页目录中设置了页表的信息,那需不需要设置页目录的信息呢?肯定是需要的,不然操作系统怎么知道页目录的基地址?那在哪里设置页目录的信息呢?答案是cr3寄存器。下面时cr3寄存器的结构。

cr3寄存器

这里PWT和PCD的意义与上面页表项的PWT和PCD的意义相同。page_directory_table baseaddress为物理地址,指向4KB对齐的页目录地址。在不使用PAE技术的情况下,有两层页表(第一层为页目录,第二层为页表)。最高层为页目录有1024项,占用4KB。

第80-81行代码把页目录基地址设置为0。

第82-84行代码设置CR0的PG位,开启分页。

经过ret指令之后我们就到了main函数里。

下面是main.c的内容。

#define PAGE_SIZE   4096

long user_stack[PAGE_SIZE >> 2];

struct {
	long * a;
	short b;
} stack_start = {&user_stack[PAGE_SIZE >> 2], 0x10};

void main(void)
{
	while (1);
}

main函数比较简单,里面是一个死循环。

接下来运行看看吧。

2.4运行结果

这里是运行到main函数的画面,右边是页表的内容,通过菜单栏View->Linear MemDump…->输入0x1000进行查看。不过等等,那里可以明确说明这里是main函数呢?以后我们会写很多C代码,我怎么知道运行到了哪里?这就要提到System.map了,这个文件在编译后会出现,以下是这小节代码编译后它的内容。

00000000 T pg_dir
00000000 T startup_32
00000000 T _text
00000047 t setup_idt
0000004f t setup_gdt
00001000 t pg0
00002000 t pg1
00003000 t pg2
00004000 t pg3
00005000 T tmp_floppy_area
00005400 t after_page_tables
00005412 t L6
00005414 t setup_paging
00005476 t idt_descr
0000547e t gdt_descr
00005488 T idt
00005c88 T gdt
00006488 T _etext
00006488 T main
000064b8 D _data
000064b8 D stack_start
000064c0 B _bss
000064c0 B _ebss
000064c0 B user_stack
000064c0 D _edata
000074c0 B _end

可以看到main函数的地址是0x6488,与bochs界面上的地址相符。不仅如此,我们还可以看到pg_dir,pg0,pg1,pg2,pg3的地址。这里文件在以后调试过程中大有帮助。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值