之前已经完成了最基本的从实模式-》保护模式,跳出了512字节限制,开始走向了kernel的跳转之路。但是,问题也出来了,先贴出一个最最基础的kernel原型:
; DD's kernel. @2013-04-29
;以下常量定义部分。内核的大部分内容都应当固定
core_code_seg_sel equ 0x38 ;内核代码段选择子
core_data_seg_sel equ 0x30 ;内核数据段选择子
sys_routine_seg_sel equ 0x28 ;系统公共例程代码段的选择子
video_ram_seg_sel equ 0x20 ;视频显示缓冲区的段选择子
core_stack_seg_sel equ 0x18 ;内核堆栈段选择子
mem_0_4_gb_seg_sel equ 0x08 ;整个0-4GB内存的段的选择子
;以下是系统核心的头部,用于加载核心程序
core_length dd core_end ;核心程序总长度#00
sys_routine_seg dd section.sys_routine.start
;系统公用例程段位置#04
core_data_seg dd section.core_data.start
;核心数据段位置#08
core_code_seg dd section.core_code.start
;核心代码段位置#0c
core_entry dd start ;核心代码段入口点(段内偏移) #10
dw core_code_seg_sel ;段选择子,用于确定cs寄存
[bits 32]
SECTION sys_routine vstart=0 ;系统公共例程代码段
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;function:在保护模式下,打印一串字符串 ;
;@1 input: 要打印的字符串的物理地址, edx-段内偏移 ;
;@2 input: 打印的字符长度: ecx ;
;output: null ;
;desc: es:指向0xb800,字符显示缓冲区的选择子, ds:核心数据段的选择子 ;
;history: 增加换行功能,每次打印完,自动换行 ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
printf:
push ax
push bx
push cx
push dx
push es
push ds
;1. first of all, trans the phy addres into logic address
;mov bx, 16
;div bx
;0x30 - 核心数据段 段选择子
mov ebx, 0x30
mov ds, ebx
;0x20 - 字符显示缓冲区的选择子
mov ebx, 0x20
mov es, ebx
;首先针对字符长度,计算占用的行数
;接下去要修改dx的值,所以先push ax 以及dx
push eax
push edx
mov ax, cx ;ax是字符串长度
xor edx, edx
mov bx, 0x50
div bx ;除以一行长度80, ax保存了行数,dx保存了余数
cmp dx, 0
;如果没有余数,那么无需+1
je go
;否则需要+1
inc ax
go:
; push ds
; mov si, 0
; mov ds, si
mov [ds:CUR_ROW], ax
; pop ds
;最后pop dx以及ax
pop dx
pop ax
;cx为字符串的长度,循环次数是字符串的1/2倍
mov ax, cx
mov bl, 2
div bl
cmp ah, 0x00 ;比较是否存在余数,如果存在则需要将商(al)+1
je no ;如果不存在余数
;如果存在余数
inc al
no:
xor cx, cx
mov cl, al
mov bx, dx
;当前的id从TOTAL_ROW中获取
; push ds
; mov si, 0
; mov ds, si
mov ax, [TOTAL_ROW]
mov si, 0xA0 ;bug-fix, 这里的offset要包含两个字符之间的控制字符串,所以是0x50 * 2 = 0xA0
mul si
mov di, ax
; pop ds
lp:
;一次性取出2个字节,提高读取效率
mov ax, [bx] ;直接用dx来寻址把内存赋值到ax还不行~~ 得用bx,神囧啊
mov byte [es:di], al
inc di
mov byte [es:di], 00000100B
inc di
mov byte [es:di], ah
inc di
mov byte [es:di], 00000100B
inc di
add bx, 0x02
loop lp
;loop完成后,要把当前的di写回到CUR_INDEX中去,下次重新调用的时候,不会覆盖之前打印的信息
; inc di ;bug-fix: 这个不需要再加了,因为循环最后一次的时候,已经多加一次,然后退出的!!!
; push ds
; mov si, 0
; mov ds, si
;获取打印前占位的row index
mov si, [TOTAL_ROW]
;把当前的row index加上去
mov di, [CUR_ROW]
add si, di
mov [TOTAL_ROW], si
; pop ds
pop ds
pop es
pop dx
pop cx
pop bx
pop ax
retf
SECTION core_data vstart=0 ;系统核心的数据段
;保存当前显存区间,当前开始的ROW id,从0开始
TOTAL_ROW: dw 0x0000
CUR_ROW: dw 0x0000
TEST_STRING db "hello, here's the kernel" ;24
SECTION core_code vstart=0 ;系统代码段
start:
mov ecx, 24
mov ebx, 0x30
mov ds, bx
mov edx, TEST_STRING
call sys_routine_seg_sel:printf
hlt
;===============================================================================
SECTION core_trail
;-------------------------------------------------------------------------------
core_end:
print函数是之前在实模式下就写好了的,现在只是稍微修改了下代码以更好的适配保护模式。但是把代码一跑,烦心的问题出来了:“check_cs(0x0018): not a valid code segment! ”。
<bochs:1>
省略
00017824978i[BIOS ] Booting from 0000:7c00
00017826445e[CPU0 ] check_cs(0x0018): not a valid code segment !
00017826445e[CPU0 ] interrupt(): gate descriptor is not valid sys seg (vector=0x0d)
00017826445e[CPU0 ] interrupt(): gate descriptor is not valid sys seg (vector=0x08)
00017826445i[CPU0 ] CPU is in protected mode (active)
00017826445i[CPU0 ] CS.mode = 32 bit
00017826445i[CPU0 ] SS.mode = 32 bit
00017826445i[CPU0 ] EFER = 0x00000000
00017826445i[CPU0 ] | EAX=00000018 EBX=00000004 ECX=00000000 EDX=00000000
00017826445i[CPU0 ] | ESP=fffffff4 EBP=00000000 ESI=000e0001 EDI=00000001
00017826445i[CPU0 ] | IOPL=0 id vip vif ac vm RF nt of df if tf sf zf af pf cf
00017826445i[CPU0 ] | SEG sltr(index|ti|rpl) base limit G D
00017826445i[CPU0 ] | CS:0028( 0005| 0| 0) 00040018 000c00a9 0 1
00017826445i[CPU0 ] | DS:0004( 0000| 1| 0) f053f000 0000ff53 0 0
00017826445i[CPU0 ] | SS:0018( 0003| 0| 0) 00007c00 f1000fff 1 1
00017826445i[CPU0 ] | ES:0030( 0006| 0| 0) 000400c0 000c001d 0 1
00017826445i[CPU0 ] | FS:0000( 0005| 0| 0) 00000000 0000ffff 0 0
00017826445i[CPU0 ] | GS:0000( 0005| 0| 0) 00000000 0000ffff 0 0
00017826445i[CPU0 ] | EIP=000000a5 (000000a5)
00017826445i[CPU0 ] | CR0=0x60000011 CR2=0x00000000
00017826445i[CPU0 ] | CR3=0x00000000 CR4=0x00000000
(0).[17826445] [0x00000000000400bd] 0028:00000000000000a5 (unk. ctxt): retf
00017826445e[CPU0 ] exception(): 3rd (13) exception with no resolution, shutdown status
00017826445i[SYS ] bx_pc_system_c::Reset(HARDWARE) called
00017826445i[CPU0 ] cpu hardware reset
00017826445i[APIC0] allocate APIC id=0 (MMIO enabled) to 0x00000000fee00000
00017826445i[CPU0 ] CPUID[0x00000000]: 00000002 756e6547 6c65746e 49656e69
00017826445i[CPU0 ] CPUID[0x00000001]: 00000633 00010800 00002008 1fcbfbff
00017826445i[CPU0 ] CPUID[0x00000002]: 00410601 00000000 00000000 00000000
00017826445i[CPU0 ] CPUID[0x80000000]: 80000008 00000000 00000000 00000000
00017826445i[CPU0 ] CPUID[0x80000001]: 00000000 00000000 00000101 2a100000
00017826445i[CPU0 ] CPUID[0x80000002]: 20202020 20202020 20202020 6e492020
00017826445i[CPU0 ] CPUID[0x80000003]: 286c6574 50202952 69746e65 52286d75
00017826445i[CPU0 ] CPUID[0x80000004]: 20342029 20555043 20202020 00202020
00017826445i[CPU0 ] CPUID[0x80000005]: 01ff01ff 01ff01ff 40020140 40020140
00017826445i[CPU0 ] CPUID[0x80000006]: 00000000 42004200 02008140 00000000
00017826445i[CPU0 ] CPUID[0x80000007]: 00000000 00000000 00000000 00000000
00017826445i[CPU0 ] CPUID[0x80000008]: 00003028 00000000 00000000 00000000
00017826445i[ ] reset of 'pci' plugin device by virtual method
00017826445i[ ] reset of 'pci2isa' plugin device by virtual method
00017826445i[ ] reset of 'cmos' plugin device by virtual method
00017826445i[ ] reset of 'dma' plugin device by virtual method
00017826445i[ ] reset of 'pic' plugin device by virtual method
00017826445i[ ] reset of 'pit' plugin device by virtual method
00017826445i[ ] reset of 'floppy' plugin device by virtual method
00017826445i[ ] reset of 'vga' plugin device by virtual method
00017826445i[ ] reset of 'acpi' plugin device by virtual method
00017826445i[ ] reset of 'ioapic' plugin device by virtual method
00017826445i[ ] reset of 'keyboard' plugin device by virtual method
00017826445i[ ] reset of 'harddrv' plugin device by virtual method
00017826445i[ ] reset of 'pci_ide' plugin device by virtual method
00017826445i[ ] reset of 'unmapped' plugin device by virtual method
00017826445i[ ] reset of 'biosdev' plugin device by virtual method
00017826445i[ ] reset of 'speaker' plugin device by virtual method
00017826445i[ ] reset of 'extfpuirq' plugin device by virtual method
00017826445i[ ] reset of 'parallel' plugin device by virtual method
00017826445i[ ] reset of 'serial' plugin device by virtual method
00017826445i[ ] reset of 'gameport' plugin device by virtual method
00017826445i[ ] reset of 'iodebug' plugin device by virtual method
Next at t=17826446
(0) [0x00000000fffffff0] f000:fff0 (unk. ctxt): jmp far f000:e05b ; ea5be000f0
<bochs:2>
快速的走读一遍,没有发现什么大的问题,无奈只能单步进去看看到底程序跑到哪里去了,导致cs不合法,之所以提示不合法,那是因为bochs检测出cs的值为0x18,看看源代码,你就会发现段选择子 = 0x18是 "core_stack_seg_sel equ 0x18 ;内核堆栈段选择"。那么,毫无疑问,堆栈段被当成cs,那肯定是非法的。
火速单步:发现代码跑到retf后,提示了错误。
<bochs:25> u /10
000400b3: ( ): pop ds ; 1f
000400b4: ( ): pop es ; 07
000400b5: ( ): pop dx ; 665a
000400b7: ( ): pop cx ; 6659
000400b9: ( ): pop bx ; 665b
000400bb: ( ): pop ax ; 6658
000400bd: ( ): retf ; cb
000400be: ( ): add byte ptr ds:[eax], al ; 0000
000400c0: ( ): add dword ptr ds:[eax], eax ; 0100
000400c2: ( ): add dword ptr ds:[eax], eax ; 0100
<bochs:26>
SECTION core_code vstart=0 ;系统代码段
start:
mov ecx, 24
mov ebx, 0x30
mov ds, bx
mov edx, TEST_STRING
call sys_routine_seg_sel:printf
hl
但是检查了下调用前以及调用printf,跳转到该代码段的cs,都是正常的,但是为啥retf后,cs寄存器不恢复成之前的0x38,而变成了0x18?这个非常的令人匪夷所思。百思不得其解啊,混蛋。
这种情况下,最好的办法就是再检查下the fucking code:
;接下去要修改dx的值,所以先push ax 以及dx
push eax
push edx
........
;最后pop dx以及ax
pop dx
pop ax
很明显,push和pop没有对称,这是一个严重的问题,为何说严重呢? 原因很简单:会破坏原来的栈,带来的后果要么就是自己push的东东没有清理干净,导致后来push的人需要为此买单,要么就是多pop了,把别人的push的东西给破坏了。总之一句话,非常严重!
初步定位完毕,现在需要具体的分析,在这种错误的代码下,为何会引起cs异常:
这里需要一点背景:call aaa:bbb, 这个是典型的跨段调用;那么call之前会做啥事情呢?很简单,call意味着往别的代码段跳转,而这个跳转不是一去不返的,必须得返回,其实就是函数必须要返回,不论是否存在返回值,或者是void的。那么,跳转过去是简单的,有aaa:bbb指引,问题在于跳转过去后,cs选择子肯定被改写成了当前的aaa,ip也被写成了bbb,那么cpu怎么知道在执行完整个call后,如何返回? 那么ss指引的栈就是发挥作用了,它负责为华丽的call后擦屁股,怎么擦?那么必须要在call前,做两件事情:1、把call前的cs寄存器值push到ss指引的栈中 2、把call执行完后紧接着的指令地址push到栈中(注意,是紧接着的ip哦~~)。这样子,call的最后部分,肯定会有一个ret或者retf(跨段),返回后,原本的那两个push值被pop到cs以及ip寄存器中。(这个动作应该是ret触发的)。那么程序又可以正常执行下去了。
元归正传:根据代码,我们先dump call之前的栈内存:
<bochs:3> u /2
000400ed: ( ): call far 0028:00000000 ; 9a000000002800
000400f4: ( ): hlt ; f4
<bochs:56> xp /310bx 0x7ad0
[bochs]:
0x0000000000007ad0 <bogus+ 0>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007ad8 <bogus+ 8>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007ae0 <bogus+ 16>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007ae8 <bogus+ 24>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007af0 <bogus+ 32>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007af8 <bogus+ 40>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b00 <bogus+ 48>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b08 <bogus+ 56>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b10 <bogus+ 64>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b18 <bogus+ 72>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b20 <bogus+ 80>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b28 <bogus+ 88>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b30 <bogus+ 96>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b38 <bogus+ 104>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b40 <bogus+ 112>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b48 <bogus+ 120>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b50 <bogus+ 128>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b58 <bogus+ 136>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b60 <bogus+ 144>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b68 <bogus+ 152>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b70 <bogus+ 160>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b78 <bogus+ 168>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b80 <bogus+ 176>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b88 <bogus+ 184>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b90 <bogus+ 192>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007b98 <bogus+ 200>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007ba0 <bogus+ 208>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007ba8 <bogus+ 216>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007bb0 <bogus+ 224>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007bb8 <bogus+ 232>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007bc0 <bogus+ 240>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007bc8 <bogus+ 248>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007bd0 <bogus+ 256>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007bd8 <bogus+ 264>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007be0 <bogus+ 272>: 0x04 0x00 0x00 0x00 0x04 0x00 0x00 0x00
0x0000000000007be8 <bogus+ 280>: 0x30 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x0000000000007bf0 <bogus+ 288>: 0x00 0x80 0x09 0x00 0x00 0x00 0x04 0x00
0x0000000000007bf8 <bogus+ 296>: 0x18 0x00 0x00 0x00 0x4c 0x01 0x00 0x00
0x0000000000007c00 <bogus+ 304>: 0x31 0xc0 0x8e 0xd8 0x31 0xc0
单步执行,进入call后的代码,下面是函数printf内,所有push完成后的stack memory dump:
为了方便突出问题的来龙去脉,忽略中间部分处理的代码,只突出pop的代码:
;最后pop dx以及ax
pop dx
pop ax
pop ds
pop es
pop dx
pop cx
pop bx
pop ax
好了,现在终于可以看看这个stack memory怎么被破坏了~
上图 注意:p-ax表示pop ax,由于pop的时候,eax变成ax,长度缩减了一半,所以当pop完ax,需要retf的时候,pop的ip变成了0x00040030,而pop的cs变成了0x00000018。这也就印证了最开始bochs提示的cs异常,说白了,起始就是由于eax edx缩水成为ax,dx,导致整个栈内存pop都前移了4个字节,cs变成了原来的ip,因此cs被置成原ip的0x18的时候就异常了。
00017826445e[CPU0 ] check_cs(0x0018): not a valid code segment !
另外,这里需要注意,为什么ip是0x18? 回忆一下之前说的,ret/retf后,ip应该是call前的执行的指令的下一条指令的地址,由于指令是顺序执行的,反汇编一把指令:
<bochs:81> u/2
000400ed: ( ): call far 0028:00000000 ; 9a000000002800
000400f4: ( ): hlt ; f4
<bochs:82>
从上面的反汇编结果来看,call与hlt指令首地址之间相差7个字节(其实就是call aaa:bbb指令的长度),执行call前的ip是0x11,所以call的下一条指令(hlt)的指令地址就是0x11+0x07 = 0x18
<bochs:82> reg
rax: 0x00000000_00000004 rcx: 0x00000000_00000018
rdx: 0x00000000_00000004 rbx: 0x00000000_00000030
rsp: 0x00000000_00000000 rbp: 0x00000000_00000000
rsi: 0x00000000_000e0001 rdi: 0x00000000_000000dc
r8 : 0x00000000_00000000 r9 : 0x00000000_00000000
r10: 0x00000000_00000000 r11: 0x00000000_00000000
r12: 0x00000000_00000000 r13: 0x00000000_00000000
r14: 0x00000000_00000000 r15: 0x00000000_00000000
rip: 0x00000000_00000011
eflags 0x00000006: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af PF cf
<bochs:83>
定位原因到此结束,把pop改成edx和eax,实际运行结果: