一、搭建第四章编译和调试
也就是搭建linux-0.00(简单多任务内核实例剖析)的环境和编译
下面我的是ubuntu12.04 x32编译
1.下载代码
下载http://www.oldlinux.org/Linux.old/bochs/linux-0.11-devel-050518.zip这也就是linux-0.00第四章末尾的程序
2.安装as86和ld86
其中boot.s是用as86/ld86编译(需要我们下载)
head.s是用gnu as/ld编译(自带)
sudo apt-get install bin86 安装as86/ld86
3.修改源码
解压在里面,这里面已经编译好了,如果已经安装bochs2.2那么就可以直接运行了,
当然我们要自己编译那么找到boot.s和head.s, Makefile这三个文件,作如下修改
[1]修改head.s,修改三处
movl scr_loc, %bx改为mov lscr_loc,%ebx
movl $65, %al 改为movb $65,%al
movl $66, %al改为movb $66, %al
把所有.align 2改成.align 4, .align 3改成.align 8
在.text中添加一行 .globl startup_32
[2]修改boot.s
rep movw 这行书上写成了一行
代码里面是两行所以不用改了
[3]修改Makefile文件,修改三处
#AS =gas gas gld is old change by snownight
#LD =gld
AS =as
LD =ld
#LDFLAGS =-s-x -M
LDFLAGS=-m elf_i386 -Ttext 0 -e startup_32
all: Image
Image: boot system
ddbs=32 if=boot of=Image skip=1
#ddbs=512 if=system of=Image skip=2 seek=1
ddbs=512 if=system of=Image skip=8 seek=1 把head.s写到0xe00处
sync
[4]编译后发现image比以前的多了0x440个字节,别的地方差别很小,不影响我们的运行
4.调试
新建run.bat文件内容如下
"C:\Program Files(x86)\Bochs-2.2\bochsdbg" -q -f bochsrc-0.00.bxrc
二、简单多任务内核实例剖析
总共有三个文件boot.s和head.s,Makefile
image文件内容分布
代码执行过程内存移动情况
内核任务在虚拟机地址空间的分配图
内存数据分布
执行结果
代码详细注释
其实就是boot.s把head.s载入到内存,然后运行,有2个任务都会调用int 0x80,此时的0x80中断已经被设置成system_interrupt函数了,此函数就打印一个字符,timer_interrupt用来切换任务,切换的办法是
ljmp $TSS1_SEL, $0
popl %eax
pop %ds
iret
有一个很深的疑问,根据代码可知道打印的字符应该是没有颜色的SN,但是为何出现有颜色的SN呢!!!
1.boot.s
!boot.s
!首先利用BIOS中断把内核代码(head.s的代码)加载到内存0x10000然后移动到内存0处
! 最后进入保护模式,并跳转到内存0(head.s代码)开始继续执行
BOOTSEG = 0x07c0 !引导扇区(本程序)被BIOS加载到内存0x7c00处
SYSSEG = 0x1000 !内核(head.s)先加载到0x10000处,然后移动到0x0处
SYSLEN = 17 !内核占用的最大磁盘扇区数。
entry start
start:
jmpi go,#BOOTSEG !段间跳转至0x7c0:go处。当本程序刚运行时所有段寄存器值
go: mov ax,cs !均为0。该跳转语句会把CS寄存器加载为0x7c0(原为0)
mov ds,ax !让DS和SS都指向0x7c0段
mov ss,ax
mov sp,#0x400 !设置临时栈指针。其值需大于程序末端并有一定空间即可
!加载内核代码到内存0x10000开始处。
load_system:
mov dx,#0x0000 !利用BIOS中断int 0x13功能读取head代码,从启动盘第2扇区开始读17个扇区到内存0x10000
mov cx,#0x0002 !DH-磁头号;DL-驱动器号;CH-10位磁道号低8位;
mov ax,#SYSSEG !CL--位7、6是磁道号高2位,5-0起始扇区号(从1计)。
mov es,ax !ES:BX--读入缓冲区位置(0x1000:0x0)
xor bx,bx !AH--读扇区功能号;AL-需读的扇区数(17)
mov ax,#0x200+SYSLEN
int 0x13
jnc ok_load !若没有发生错误则跳转继续运行,否则死循环
die: jmp die
! 把内核代码移动到内存0开始处。共移动8KB字节(内核长度不超过8KB)。
ok_load:
cli !关中断
mov ax, #SYSSEG !复制开始位置DS:SI=0x1000:0;目的位置ES:DI=0:0。
mov ds, ax
xor ax, ax
mov es, ax
mov cx, #0x1000 !8KB字节
sub si,si
sub di,di
rep !重复其后的串操作指令。重复前先判断CX是否是0,为0就结束重复,
movw !否则CX减1,重复其后的串操作指令,movw是复制一个字节
!加载IDT和GDT基地址寄存器IDTR和GDTR
mov ax, #BOOTSEG
mov ds, ax
lidt idt_48 !加载IDTR。6字节操作数 2字节表示长度,4字节线性基地址。
lgdt gdt_48 !加载GDTR。6字节操作数 2字节表示长度,4字节线性基地址。
!设置控制寄存器CR0(即机器状态字),进入保护模式,段选择符值8对应GDT表中第2个段描述符,代码段。
mov ax,#0x0001 ! 在CR0中设置保护模式标志PE(位0),接下来可以进入保护模式
lmsw ax !此条指令等价于x86 masm 汇编的mov cr0,ax,
jmpi 0,8 !已经开启保护模式这里跳转,其实是在gdt中寻址,base=0x0,
!由于选择子是8也就是第2个描述符,至此跳到0x0处执行,进入head.s执行
gdt:.word 0,0,0,0 ! dummy
.word 0x07FF !8Mb - limit=2047 (2048*4096=8Mb) 第1,2字节
.word 0x0000 !base address=0x00000
.word 0x9A00 !code read/exec
.word 0x00C0 !granularity=4096, 386 第7,8字节
.word 0x07FF !8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 !base address=0x00000
.word 0x9200 !data read/write
.word 0x00C0 !granularity=4096, 386
idt_48: .word 0 ! idtlimit=0
.word 0,0 !idt base=0L
gdt_48: .word 0x7ff ! gdtlimit=2048, 256 GDT entries
.word 0x7c00+gdt,0 ! gdt base = 07xxx
.org 510
.word 0xAA55
2.head.s
# head.s包含32位保护模式初始化设置代码,时钟中断代码、系统调用中断代码和两个任务的代码
# 在初始化完成之后程序移动到任务0开始执行,并在时钟中断控制下进行任务0和1之间的切换
SCRN_SEL =0x18
TSS0_SEL =0x20
LDT0_SEL =0x28
TSS1_SEL =0X30
LDT1_SEL =0x38
.text
.globl startup_32
startup_32:
#首先加载数据段寄存器DS,堆栈段寄存器SS和堆栈指针ESP.所有段的线性基地址都是0.
movl$0x10,%eax #0x10=1000 0b是GDT中第三项数据段选择子
mov%ax,%ds
lssinit_stack,%esp #设置系统堆栈init_stack->ss:esp;
#在新的位置重新设置IDT和GDT表
#执行setup_idt后idtr=0x198,limit=0x7ff
#把256个中断门全部填入默认
#0x00000198 <bogus+ 0>: 0x14 0x01 0x08 0x00 0x00 0x8e 0x00 0x00
callsetup_idt #设置IDT,先把256个中断门都填默认处理过程的描述符
callsetup_gdt #设置GDT
movl$0x10,%eax #在改变GDT之后重新加载所有段寄存器
mov%ax,%ds
mov%ax,%es
mov%ax,%fs
mov%ax,%gs
lssinit_stack,%esp
# 设置8253定时芯片。把计数器通道0设置成每隔10毫秒向中断控制器发送一个中断请求信号。
#8254芯片的时钟输入频率为1.193180MHZ,英雌初始计数值LATCH=1193180/100,约为11931,也就是我们设置成
#这样,这货就会每隔10秒钟在通道0输出端out发出一个方波上升沿。也就是发出一个时钟中断请求IRQ0信号
movb$0x36, %al #控制字:设置通道0工作在方式3(方波发生器方式)计算初值采用二进制
movl$0x43, %edx #8253芯片控制字寄存器写端口
outb%al, %dx
movl$11930, %eax #初始计数值设置为LATCH
movl$0x40, %edx #通道0的端口
outb%al, %dx #分别两次把初始计算值写入通道0
movb%ah, %al
outb%al, %dx
#在IDT表的第8项和128项(0x80)分别设置定时中断门描述符(timer_interrupt)和系统调用陷阱门描述符(system_interrupt)
#下面执行完后
#<bochs:56>x /8bx 0x198+0x8*8
#0x000001d8<bogus+ 0>: 0x2c 0x01 0x08 0x00 0x00 0x8e 0x00 0x00
#0x000x00 x8e 0x00 0x00 0x08 0x01 0x2c
#
movl$0x00080000, %eax #中断程序属于内核,即eax高字是内核代码段选择符0x0008
movw$timer_interrupt, %ax #设置定时中断们描述符。取定时中断处理程序地址 ,timer_interrupt在内存的地址是0x12c
movw$0x8E00, %dx #中断门类型是14(屏蔽中断),特权级0或硬件使用
movl$0x08, %ecx #开机时BIOS设置的时钟中断向量号8.这里直接使用它
#此时eax=0x8012C,dx=0x8e00,ecx=0x8
#下面就是lea esi,dword ptr ds:[ecx*8+0x198]
leaidt(,%ecx,8), %esi #把IDT描述符0x08地址放入ESI中,然后设置该描述符
movl%eax,(%esi)
movl%edx,4(%esi)
#下面执行完后
#<bochs:63>x /8bx 0x198+0x8*0x80
#0x00000598<bogus+ 0>: 0x68 0x01 0x08 0x00 0x00 0xef 0x00 0x00
movw$system_interrupt, %ax #设置系统调用陷阱门描述符,取系统调用处理程序地址
movw$0xef00, %dx #陷阱门类型是15,特权3的程序可以执行
movl$0x80, %ecx #系统调用向量号是0x80
#leaesi, dword ptr ds:[ecx*8+0x198]
leaidt(,%ecx,8), %esi #把IDT描述符项0x80地址放入ESI中,然后设置该描述符
movl%eax,(%esi)
movl%edx,4(%esi)
#好了,现在我们为移动到任务0(任务A)中执行操作堆栈内容,在堆栈中人工建立中断返回时场景。
pushfl #复位标志寄存器EFLAGS中的嵌套任务标志
andl$0xffffbfff, (%esp)
popfl
movl$TSS0_SEL, %eax #把任务0的TSS段选择符加载到任务寄存器TR,(TSS0_SEL=0x20=10000 0b指向GDT第五项)
ltr%ax
movl$LDT0_SEL, %eax #把任务0的LDT段选择符加载到局部描述符表寄存器LDTR(LDT0_SEL=0x28=101 0 00b,指向GDT第六项)
lldt%ax #TR和LDTR只需人工加载一次,以后CPU会自动处理
#0008:00000092(unk. ctxt): mov dword ptr ds:0x17f, 0x0
movl$0, current #把任务号0保存在current变量中
sti #现在开启中断,并在栈中营造中断返回时的场景
pushl$0x17 #把任务0当前局部空间数据段(堆栈段)选择符入栈
pushl$init_stack #把堆栈指针入栈(也可以直接把ESP入栈)
pushfl #把标志寄存器值入栈
pushl$0x0f #把当前局部空间代码选择符入栈
pushl$task0 #把代码指针入栈
#IP<-SS:[SP],SP<-SP+2;所以就开始执行task0。
#0008:000000ac (unk. ctxt): iretd ; cf
#执行此条指令前
#<bochs:52> info cpu
#eax:0x00000028, ebx:0x00000000,ecx:0x00000080, edx:0x0000ef00
#ebp:0x00000000, esp:0x00000bc4,esi:0x00000598, edi:0x00000998
#eip:0x000000ac, eflags:0x00000246,inhibit_mask:0
#执行后
#<bochs:54> info cpu
#eax:0x00000028, ebx:0x00000000,ecx:0x00000080, edx:0x0000ef00
#ebp:0x00000000, esp:0x00000bd8,esi:0x00000598, edi:0x00000998
#eip:0x000010e0, eflags:0x00000246,inhibit_mask:0
iret #执行中断返回指令,从而切换特权级3的任务0中执行。
/****************************************/
#一下是设置GDT和IDT中描述符项的子程序
setup_gdt: #使用6字节操作数lgdt_opcode设置GDT表位置和长度
lgdtlgdt_opcode
ret
#暂时设置IDT表中所有256中断门描述符都是同一个默认值,均使用默认的中断处理过程
#ignore_int.设置的具体方法是:首先在eax和edx寄存器对中分别设置好默认中断门描述符的0-3
#字节和4-7字节的内容,然后利用该寄存器对循环往IDT表中填充默认中断描述符内容
setup_idt:
leaignore_int,%edx
movl$0x00080000,%eax #选择符位0x0008
movw%dx,%ax /* selector =0x0008 = cs */
movw$0x8E00,%dx #类型E=1110b,即中断门类型,特权级为0
leaidt,%edi
mov$256,%ecx #循环设置所有256个门描述符项
rp_sidt:
movl%eax,(%edi)
movl%edx,4(%edi)
addl$8,%edi
dec%ecx
jnerp_sidt
lidtlidt_opcode #最后用6字节操作数加载IDTR寄存器
ret
# -----------------------------------
#显示字符串自存钱,取当前光标位置并把al中的字符显示在屏幕上,整屏可以显示80X25个字符
#在80X25彩色字符模式下,显示器显示25行,每行80个字符
#每个字符占2个字节,低字节是字符的ASCII码,高字节是设置所示字符的属性
#从高位到低位 闪烁,背景红,背景绿,背景蓝,高亮,前景红,前景绿,前景蓝
write_char:
push%gs #首先保存要用到的寄存器,eax由调用者负责保存,所以无需pushl%eax
pushl%ebx
#0008:000000e8(unk. ctxt): mov ebx, 0x18
mov$SCRN_SEL, %ebx #然后让gs指向显存内存段(0xb8000),,0x18指向GDT的第4个个描述符
mov%bx, %gs
movlscr_loc, %ebx #再从变量src_loc中取目前字符串显示位置值,scr_loc初始为0
shl$1, %ebx #每2个字节表示一个字符,一个是属性一个是字符的ascii,这里属性是灰色,值是al
#movbyte ptr gs:[ebx], al
movb%al, %gs:(%ebx) #从0xb8000开始0xb8000+ebx一直写,
shr$1, %ebx #把字符放到显示内存后把位置值除2加1,此时位置值对
incl%ebx #应下一个显示位置,如果该值大于2000,则复位成0
cmpl$2000, %ebx #一个屏幕写完
jb1f
movl$0, %ebx
1: movl%ebx, scr_loc #最后把这个位置值保存起来
popl%ebx
pop%gs
ret #返回
/***********************************************/
#下面是3个中断处理程序,默认中断,定时中断和系统调用中断
.align 4
ignore_int: #默认中断处理程序,若系统产生了其它中断,则会在屏幕上显示一个字符C
push%ds
pushl%eax
movl$0x10, %eax #首先让ds指向内核数据段,因为中断程序属于内核
mov%ax, %ds
movl$67, %eax /* print 'C' */#在AL中存放字符C的代码,调用显示程序显示在屏幕上
callwrite_char #call指令是自动把eax入栈
popl%eax
pop%ds
iret
/* Timer interrupt handler */
.align 4
timer_interrupt: #定时中断处理程序,其中主要执行任务切换操作
push%ds
pushl%eax
movl$0x10, %eax
mov%ax, %ds #首先DS指向内核数据段
movb$0x20, %al
outb%al, $0x20 #然后立刻允许其他硬盘中断,即向8259A发送EOI命令
movl$1, %eax
cmpl%eax, current #接着判断当前任务,若是任务1则去执行任务0,或反之
je1f
movl%eax, current
ljmp$TSS1_SEL, $0
jmp2f
1: movl$0, current
ljmp$TSS0_SEL, $0
2: popl%eax
pop%ds
iret
/* system call handler */
#系统调用中断int 0x80处理程序,该例子只有一个显示字符功能
.align 4
system_interrupt:
push%ds
pushl%edx
pushl%ecx
pushl%ebx
pushl%eax
movl$0x10, %edx
mov%dx, %ds #让DS指向内核数据段
#[0x00000174]0008:00000174 (unk. ctxt): call .+0xe5
callwrite_char #调用write_char,显示al中的字符
popl%eax
popl%ebx
popl%ecx
popl%edx
pop%ds
iret
/*********************************************/
current:.long 0
scr_loc:.long 0
.align 4
lidt_opcode:
.word256*8-1 # idt contains 256entries
.longidt # This will be rewrite bycode.
lgdt_opcode:
.word(end_gdt-gdt)-1 # so does gdt
.longgdt # This will be rewrite bycode.
.align8
idt: .fill256,8,0 # idt isuninitialized
gdt:
.quad0x0000000000000000 /*第1个NULLdescriptor */
.quad0x00c09a00000007ff /*第2个 8Mb 0x08,base = 0x00000 */
.quad0x00c09200000007ff /*第3个 8Mb 0x10 */
.quad0x00c0920b80000002 /*第4个 screen 0x18- for display */
.word0x0068, tss0, 0xe900, 0x0 #第5个是TSS0段的描述符0x20
.word0x0040, ldt0, 0xe200, 0x0 #第6个是LDT0段的描述符0x28
.word0x0068, tss1, 0xe900, 0x0 #第7个 TSS1 descr0x30
.word0x0040, ldt1, 0xe200, 0x0 #第8个 LDT1 descr0x38
end_gdt:
.fill128,4,0 #初始化内核堆栈空间
init_stack: #刚进入保护模式时用于加载SS:ESP堆栈指针值
.longinit_stack #堆栈段偏移位置
.word0x10 #堆栈段同内核数据段
/*************************************/
#下面是任务0的LDT表段中的局部描述符
.align 8
ldt0:
.quad0x0000000000000000 #第一个 NULL
.quad0x00c0fa00000003ff #第2个局部代码段描述符,对应选择符是0x0f
.quad0x00c0f200000003ff #第3个局部数据段描述符,对应选择符是0x17
#下面是任务0的TSS段的内容,注意其中标号等字段在任务切换时不会改变
tss0:
.long0 /* back link */
.longkrn_stk0, 0x10 /* esp0,ss0 */
.long0, 0, 0, 0, 0 /* esp1, ss1,esp2, ss2, cr3 */
.long0, 0, 0, 0, 0 /* eip,eflags, eax, ecx, edx */
.long0, 0, 0, 0, 0 /* ebx esp,ebp, esi, edi */
.long0, 0, 0, 0, 0, 0 /* es,cs, ss, ds, fs, gs */
.longLDT0_SEL, 0x8000000 /* ldt, tracebitmap */
.fill128,4,0
krn_stk0:
# .long0
/************************************/
#下面是任务1的LDT表段内容和TSS段内容
.align 8
ldt1:
.quad0x0000000000000000 #第一个 null
.quad0x00c0fa00000003ff # 0x0f, base =0x00000
.quad0x00c0f200000003ff # 0x17
tss1:
.long0 /* back link */
.longkrn_stk1, 0x10 /* esp0,ss0 */
.long0, 0, 0, 0, 0 /* esp1, ss1,esp2, ss2, cr3 */
.longtask1, 0x200 /* eip, eflags */
.long0, 0, 0, 0 /* eax, ecx, edx, ebx*/
.longusr_stk1, 0, 0, 0 /* esp,ebp, esi, edi */
.long0x17,0x0f,0x17,0x17,0x17,0x17 /* es, cs, ss, ds, fs, gs */
.longLDT1_SEL, 0x8000000 /* ldt, tracebitmap */
.fill128,4,0 #任务1的内核栈空间,其用户栈直接使用初始化栈空间
krn_stk1:
/************************************/
#下面是任务0和任务1的程序,循环显示A和B
task0:
movl$0x17, %eax # [0x000010e5]000f:000010e5 (unk. ctxt): mov ds, ax
movw%ax, %ds #首先让DS指向任务的局部数据段
movb$0x53,%al /* print 'S' */ #要显示的字符C放入al,等下用系统调用显示
int$0x80 #执行系统调用显示字符,也就是执行函数system_interrupt
movl$0xfff, %ecx #循环执行,起延时作用
1: loop1b
#000f:000010f2 (unk. ctxt): jmp .+0x10e0
jmptask0
task1:
movl$0x17, %eax
movw%ax, %ds
movb$0x4E, %al /* print 'N' */
int$0x80 #执行系统调用显示字符
movl$0xfff, %ecx
1: loop1b
jmptask1
.fill128,4,0 #这是任务1的用户栈空间
usr_stk1:
参考文章
http://blog.chinaunix.net/uid-23917107-id-3191402.htmlubuntu-11下编译Linux0.00内核(基于赵博士提供的源码包:linux-0.00-041217和linux-0.00-050613