Linux 0.00 的编译、运行、源码下载:
http://blog.csdn.net/longintchar/article/details/78757065
Linux 0.00 Makefile 解读:
http://blog.csdn.net/longintchar/article/details/78857966
Linux 0.00 代码解析——boot.s
:
http://blog.csdn.net/longintchar/article/details/78766916
Linux-0.00
的代码分为两部分——boot.s
和head.s
.
boot.s
采用as86语言编写,是引导启动程序,先把内核代码加载到物理地址0x10000处,然后把内核代码移动到物理地址0处,接下来设置临时GDT表等信息,再把处理器设置成保护模式,最后跳转到内核代码处(0地址)运行。
head.s
是内核代码,采用GNU as汇编语言编写,实现了2个运行在特权及3上的任务,它们在时钟中断控制下相互切换运行,一个在屏幕上打印“A”,另一个在屏幕上打印“B”。
本文要分析的是head.s
。请注意,这段代码在运行的时候,它的起始位置在物理地址0处。
1. 设置DS,ES,SS,ESP
1
2
3
4
5
SCRN_SEL = 0x18
TSS0_SEL = 0x20
LDT0_SEL = 0x28
TSS1_SEL = 0X30
LDT1_SEL = 0x38
SCRN_SEL
等都是符号常量,代表某选择子的值,这样写可读性好。相当于c语言中的#define SCRN_SEL 0x18
.
1
2
3
4
5
6
.code32
.global startup_32
.text
startup_32:
movl $0x10,%eax
mov %ax,%ds
.code32
是我自己加的,不然编译会报错。这句伪指令告诉编译器,下面的代码要编译成32位代码。
.global
表示标识符是外部的或者全局的。
.text
标识正文段的开始,并切换到text
段。
movl $0x10,%eax
, 0x10是数据段(在boot.s文件中定义)的选择子,此数据段的基地址为0,界限值是0x7FF(10进制2047),粒度4KB;因为粒度是4KB,所以段长度是(2047+1)*4KB=8MB;DPL=0,向上扩展,可读可写。
mov %ax,%ds
,加载ds。
1
lss init_stack,%esp
init_stack
处有6个字节,见
1
2
3
init_stack: # Will be used as user stack for task0.
.long init_stack
.word 0x10
这是一个远指针,前4个字节是偏移,后2个字节是段选择子,这句代码表示用偏移加载esp,用数据段选择子0x10加载ss.
2. setup_idt
此过程用于在IDT(中断描述符表)中安装中断门。代码是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setup_idt:
lea ignore_int,%edx
movl $0x00080000,%eax
movw %dx,%ax /* selector = 0x0008 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
lidt lidt_opcode
ret
中断门描述符
中断门描述符如下图:
下面是低32位(代码中用eax存储),上面是高32位(代码中用edx存储)。
可以看出,中断门定义了一个长指针(段选择符:过程入口点偏移值),当发生中断的时候,处理器使用这个长指针把程序执行权转移到中断处理过程中。
在edx和eax中组装中断门描述符
lea
指令是取有效地址(偏移值)。lea ignore_int,%edx
表示把ignore_int
处的有效地址传给edx. 注意,是取ignore_int
处的偏移地址,而不是ignore_int
处存储的内容。这样,过程入口点偏移值31-16组装完毕。
movl $0x00080000,%eax
, 段选择符(=0x08,索引1,内核代码段)组装完毕。
movw %dx,%ax
, 过程入口点偏移值15-0组装完毕。
movw $0x8E00,%dx
edx的低16位组装完毕。
中断处理过程就是ignore_int
,用于在屏幕上打印一个’c’.
1
2
3
4
5
6
7
8
9
10
ignore_int:
push %ds
pushl %eax
movl $0x10, %eax
mov %ax, %ds #上一行和此行用内核数据段加载ds
movl $67, %eax #打印字符'c',实际上用AL来传参
call write_char #调用过程 write_char
popl %eax
pop %ds
iret
注意:write_char
这个过程没有指定DS
,但是确引用了DS
,比如指令movl scr_loc, %ebx
. 所以在调用write_char
之前,一定要给DS
赋合适的值。
write_char这个过程,我已经在代码后面添加了注释。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
write_char:
push %gs
pushl %ebx
mov $SCRN_SEL, %ebx #SCRN_SEL是显存段的选择子
mov %bx, %gs #gs指向显存段
movl scr_loc, %ebx #scr_loc处存放的是显示位置
shl $1, %ebx #ebx*2,得到偏移,因为一个字符用2个字节来描述
movb %al, %gs:(%ebx) #al中是字符的ASCII码,属性用默认的
shr $1, %ebx #还原ebx
incl %ebx #ebx自增1,算出下一个位置
cmpl $2000, %ebx #比较ebx和2000
jb 1f #若 ebx < 2000 则跳转到1
movl $0, %ebx #说明ebx==2000,因为位置只有0~1999,所以把ebx置为0
1: movl %ebx, scr_loc #把ebx存入scr_loc处,更新显示位置
popl %ebx
pop %gs
ret
可以看出,write_char的功能是把AL中的字符打印到屏幕上。
位置由scr_loc处存储的4字节的值指定(实际上取值0~1999),打印后更新位置(计数加1)。
1
scr_loc:.long 0 #代码中留出了4字节存放位置
填写IDT
lea idt,%edi
表示把idt
处的有效地址加载到edi.
idt标号处的代码是:
1
2
.align 8
idt: .fill 256,8,0
fill伪指令的格式是
.fill repeat,size,value
表示产生repeat个大小为size字节的重复拷贝。size最大是8,size字节的值是value.
所以,.fill 256,8,0
表示产生8*256字节,全部用0填充。IDT最多可有256个描述符,每个描述符占8个字节。
1
2
3
4
5
6
7
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx #当ecx为0时,会使ZF=0
jne rp_sidt #若ZF!=0则跳转到rp_sidt
Intel语法的间接内存引用的格式为:
section:[base + index * scale + displacement]
而在AT&T语法中对应的形式为:
section:displacement (base, index, scale)
其中,base和index是任意的32-bit base和index寄存器。scale可以取值1,2,4,8。如果不指定scale值,则默认值为1。section可以指定任意的段寄存器作为段前缀,默认的段寄存器在不同的情况下不一样。
举例:
Intel | AT&T |
---|---|
[base + index * scale + displacement] | section:displacement(base, index, scale) |
[eax + _variable] | _variable(%eax) |
[eax * 4 + _array] | _array(, %eax, 4) |
[ebx + eax * 8 + _array] | _array(%ebx, %eax, 8) |
所以,movl %eax,(%edi)
表示把eax的值传送到地址edi处,即用eax填充IDT表的0~3字节;movl %edx,4(%edi)
表示把edx的值传送到地址[edi+4]处,即用edx填充IDT表的4~7字节;这样,IDT表中第0个中断门就安装好了。同理,循环安装,一共是256个中断门。
加载IDTR
lidt lidt_opcode
加载IDTR寄存器,lidt_opcode处定义了6个字节。前2字节是界限值,界限值是表的总长度减去1;后4字节是IDT的线性基地址。因为本文件运行时的起始地址就在物理地址0处,所以线性基地址就是idt表示的值。
1
2
3
lidt_opcode:
.word 256*8-1 # idt contains 256 entries
.long idt # This will be rewrite by code.
3. setup_gdt
这个过程就一句话
1
2
3
setup_gdt:
lgdt lgdt_opcode
ret
加载GDTR寄存器。
看一下 lgdt_opcode 处都有什么:
1
2
3
lgdt_opcode:
.word (end_gdt-gdt)-1
.long gdt # This will be rewrite by code.
前2字节是GDT的界限值,后4字节是GDT的线性基地址。
1
2
3
4
5
6
7
8
9
10
11
gdt:
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a00000007ff /* 8Mb 0x08, base = 0x00000 */
.quad 0x00c09200000007ff /* 8Mb 0x10 */
.quad 0x00c0920b80000002 /* screen 0x18 - for display */
.word 0x0068, tss0, 0xe900, 0x0 # TSS0 descr 0x20
.word 0x0040, ldt0, 0xe200, 0x0 # LDT0 descr 0x28
.word 0x0068, tss1, 0xe900, 0x0 # TSS1 descr 0x30
.word 0x0040, ldt1, 0xe200, 0x0 # LDT1 descr 0x38
end_gdt:
一共定义了8个段描述符。
索引号 | 选择子 | 描述符类型 | 基地址 | 段界限 | 粒度 | P | DPL | 备注 |
---|---|---|---|---|---|---|---|---|
0 | - | 空描述符 | - | - | - | - | - | - |
1 | 0x08 | 代码段 | 0 | 0X7FF | 4KB | 1 | 0 | 内核代码段,非一致性,可读 |
2 | 0x10 | 数据段 | 0 | 0X7FF | 4KB | 1 | 0 | 内核数据段,向上扩展,可写 |
3 | 0x18 | 数据段 | 0XB8000 | 0X2 | 4KB | 1 | 0 | 内核显存段,向上扩展,可写 |
4 | 0x20 | TSS段 | tss0 | 0X68 | 1B | 1 | 3 | 任务0的TSS段,不忙 |
5 | 0x28 | LDT段 | ldt0 | 0X40 | 1B | 1 | 3 | 任务0的LDT描述符 |
6 | 0x30 | TSS段 | tss1 | 0X68 | 1B | 1 | 3 | 任务1的TSS段,不忙 |
7 | 0x38 | LDT段 | ldt1 | 0X40 | 1B | 1 | 3 | 任务1的LDT描述符 |
4. 重新加载段寄存器
因为GDT的内容改变了,所以应该重新加载所有段寄存器。
1
2
3
4
5
6
movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt.
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss init_stack,%esp
注意:因为内核代码段和boot.s
文件中的定义一样,所以不用重新加载CS
5. 设置定时芯片8253
关于这个定时芯片可以参考我的博文http://blog.csdn.net/longintchar/article/details/78885556
Intel 8253芯片是可编程计数器/定时器。该芯片提供了3个独立的16位计数器通道,每个通道可以工作在不同的工作方式下。通过向8253写入一个控制字和一个初始计数值,就可以使它开始计数。
控制字格式如下图:
代码中写入的控制字是0x36,选中通道0,先读写低字节再读写高字节,工作方式3,采用二进制计数。
通道0的端口是0x40, 先向其写入初始计数值的低字节,再写入初始计数值的高字节。
假设N为初始计数值。在工作方式3下,方波的频率是输入时钟频率的N分之一,又因为计数器的输入时钟频率是1.193180MHz=1193180Hz,所以
1
21193180/N = 方波的频率(Hz)
movl $11930, %eax
表示计数值N=11930,1193180/11930约等于100,
所以方波的频率是100Hz,周期是10ms,也就是10ms产生一个方波上升沿,此上升沿可以产生中断请求,即10ms产生一次中断。
1
2
3
4
5
6
7
8
9
# setup up timer 8253 chip.
movb $0x36, %al # al中是控制字
movl $0x43, %edx # 端口是0x43
outb %al, %dx # 把al中的控制字写入端口0x43
movl $11930, %eax # timer frequency 100 HZ
movl $0x40, %edx # 端口是0x40
outb %al, %dx # 先写低字节
movb %ah, %al # 再写高字节
outb %al, %dx
【未完待续】
最后
以上就是斯文小蜜蜂最近收集整理的关于Linux0.00 代码解析(二)的全部内容,更多相关Linux0.00内容请搜索靠谱客的其他文章。
发表评论 取消回复