分析Windows启动过程(一)
没看过上一篇别过来.
接着上一篇,
确定了引导用的(活动分区)分区之后到文件偏移0x34的位置
此时bp
为DPT记录的偏移
代码如下:
00000034 885600 mov [bp+0x0],dl
00000037 55 push bp
00000038 C6461105 mov byte [bp+0x11],0x5
0000003C C6461000 mov byte [bp+0x10],0x0
由于上面没有初始化dl或dx有关的代码, 笔者认为这是置bp处的一个字节为0(也就是设置当前分区在内存中的记录为非活动分区)
然后push bp
将bp压入栈, 接着把内存中下一个分区记录的开始磁头设置为0x05, 把分区类型设置为0(非活动分区)
然后看代码:
00000040 B441 mov ah,0x41
00000042 BBAA55 mov bx,0x55aa
00000045 CD13 int 0x13
这里是一个int 0x13
的中断调用, 根据int-13上的信息, 我们可以找到这个页面:Int 13/AH=41h/BX=55AAh
这个页面的要求和上面的代码一模一样, AH为0x41, BX为0x55AA, 看看他是干嘛的吧.
它的名称是IBM / MS INT 13扩展-安装检查
用于检测一个设备是否安装了这个扩展(可是我还是不明白指定设备用的dl在哪指定)
然后两行代码:
00000047 5D pop bp
00000048 720F jc 0x59
这里从堆栈恢复bp(见00000037), 根据上面那个int 0x13
操作的描述, 如果不支持扩展则设置CF, 所以如果不支持扩展则跳到0x59
如果支持扩展, 则会经过下面这段代码:
0000004A 81FB55AA cmp bx,0xaa55
0000004E 7509 jnz 0x59
00000050 F7C10100 test cx,0x1
00000054 7403 jz 0x59
00000056 FE4610 inc byte [bp+0x10]
根据描述, 如果支持扩展并且已安装, 则bx被置为0xaa55, 所以这里前两句判断是否已安装(提示: JNZ == JNE), 没有安装也跳转到0x59
然后比较cx和0x1, 根据说明, cx保存了支持的扩展API, 0x1的二进制是00000001, 这两行里, 如果cx == 0x1则跳转0x59, 所以仅cx != 0b00000001时才会继续, 那么00000001对应什么呢?对应着removable drive controller functions supported
, 也就是说, 如果只支持这种功能, 那也没用
如果有用(支持并安装了扩展API)则执行inc byte [bp+0x10]
, 这句代码把[bp+0x10]
作为一个字节 +1, 也就是让引导分区的分区起始相对扇区+0xff
接着代码:
00000059 6660 pushad
0000005B 807E1000 cmp byte [bp+0x10],0x0
0000005F 7426 jz 0x87
00000061 666800000000 push dword 0x0
00000067 66FF7608 push dword [bp+0x8]
0000006B 680000 push word 0x0
0000006E 68007C push word 0x7c00
00000071 680100 push word 0x1
00000074 681000 push word 0x10
00000077 B442 mov ah,0x42
00000079 8A5600 mov dl,[bp+0x0]
0000007C 8BF4 mov si,sp
0000007E CD13 int 0x13
00000080 9F lahf
00000081 83C410 add sp,byte +0x10
00000084 9E sahf
00000085 EB14 jmp short 0x9b
00000087 B80102 mov ax,0x201
0000008A BB007C mov bx,0x7c00
0000008D 8A5600 mov dl,[bp+0x0]
00000090 8A7601 mov dh,[bp+0x1]
00000093 8A4E02 mov cl,[bp+0x2]
00000096 8A6E03 mov ch,[bp+0x3]
00000099 CD13 int 0x13
0000009B 6661 popad
可以将此段代码看作一个过程, pushad
保存当前环境, 最后popad
恢复环境, 然后看中间的代码.
0000005B 807E1000 cmp byte [bp+0x10],0x0
0000005F 7426 jz 0x87
这两段代码判断是否支持扩展API, 不支持则跳转到0x87
如果不跳转, 则执行这段代码:
00000061 666800000000 push dword 0x0
00000067 66FF7608 push dword [bp+0x8]
0000006B 680000 push word 0x0
0000006E 68007C push word 0x7c00
00000071 680100 push word 0x1
00000074 681000 push word 0x10
00000077 B442 mov ah,0x42
00000079 8A5600 mov dl,[bp+0x0]
0000007C 8BF4 mov si,sp
0000007E CD13 int 0x13
00000080 9F lahf
00000081 83C410 add sp,byte +0x10
00000084 9E sahf
00000085 EB14 jmp short 0x9b
这段代码将0x00推入栈, 将引导分区的结束柱面推入栈, 推入0x00, 0x7c00, 0x1, 0x10, 然后调用了一个int 0x13
的中断, 这个中断的信息在Int 13/AH=42h这里, 是"扩展读取", DL指向读取的设备, si指向一个设备地址包, 这里si指向sp, 也就是栈顶, 所以上面的push
实际上是在构造一个disk address packet
从后面看起, 最后面push了0x10, 而且是个word, 很好地处理了一个字节的保留字节
倒数第二个push是0x1, 根据描述02h WORD number of blocks to transfer (max 007Fh for Phoenix EDD)
, 0x1是指传输块数, 然后是两个word, 这两个word并成一个dword指向缓冲区, 而缓冲区地址就是0x7c00, 现在明白了为什么一开始要复制引导扇区到0x600了吧! 代码顶上两个dword组成一个qword, 根据定义08h QWORD starting absolute block number
,
这个qword指向起始绝对块号, 而它的值是分区的分区起始相对扇区号, 也就是说, 读取分区的第一个扇区到0x7c00!!!
00000080 9F lahf
00000081 83C410 add sp,byte +0x10
00000084 9E sahf
这三行代码我就不知道是干嘛的了, 知道的说一下, 我觉得像是给PF标志位置位.
最后是jmp short 0x9b
也就是跳转到这个过程的末尾
回到上面, 如果不支持扩展API, 那么会怎么样呢?
如果不支持扩展API则会执行:
00000087 B80102 mov ax,0x201
0000008A BB007C mov bx,0x7c00
0000008D 8A5600 mov dl,[bp+0x0]
00000090 8A7601 mov dh,[bp+0x1]
00000093 8A4E02 mov cl,[bp+0x2]
00000096 8A6E03 mov ch,[bp+0x3]
00000099 CD13 int 0x13
看到int
第一时间就应该查文档, 然而笔者看到那句mov ax, 0x201
头瞬间炸了, 这什么操作?
想了想, 这应该等于mov ah, 0x2
和mov al, 0x01
, 看来微软为了节约空间真是不惜所措啊
于是找到了Int 13/AH=02h这个页面
这个调用用于读取扇区到内存
AL是要读取的扇区数, 1个
CH起始柱面号, CL起始扇区号, DH磁头号, DL设备号
Ok, 所以这些数据都是从分区记录里读取的, 而BX指向数据缓冲区, 0x7c00
接着看后面的代码:
0000009D 731C jnc 0xbb
如果没出错, 那么跳转到0xbb, 因为如果int 0x13
出错, CF标志位会被置位
先看错误处理程序:
0000009F FE4E11 dec byte [bp+0x11]
000000A2 750C jnz 0xb0
000000A4 807E0080 cmp byte [bp+0x0],0x80
000000A8 0F848A00 jz near 0x136
000000AC B280 mov dl,0x80
000000AE EB84 jmp short 0x34
000000B0 55 push bp
000000B1 32E4 xor ah,ah
000000B3 8A5600 mov dl,[bp+0x0]
000000B6 CD13 int 0x13
000000B8 5D pop bp
000000B9 EB9E jmp short 0x59
dec byte [bp+0x11]
将分区起始相对扇区号 -1
jnz 0xb0
如果分区起始相对扇区号 == 0则跳转到0xb0
如果不跳转则执行:
000000A4 807E0080 cmp byte [bp+0x0],0x80
000000A8 0F848A00 jz near 0x136
000000AC B280 mov dl,0x80
000000AE EB84 jmp short 0x34
检查设备编号是否0x80, 是则直接跳转到0x136
然后将dl设置为0x80, 之后jmp回到之前刚刚找到活动分区的地方, 我认为启动过程中, 第一次都会先执行到这里, 然后下一次才有可能成功
那如果跳到0x136呢?
00000136 A0B607 mov al,[0x7b6]
00000139 EB03 jmp short 0x13e
打印0x7b6的文字(但我没看出有什么文字在那里)
如果如果分区起始相对扇区号 == 0, 则执行:
000000B0 55 push bp
000000B1 32E4 xor ah,ah
000000B3 8A5600 mov dl,[bp+0x0]
000000B6 CD13 int 0x13
000000B8 5D pop bp
000000B9 EB9E jmp short 0x59;
push bp
和pop bp
用于保护bp的值
xor ah,ah
同等于mov ah, 0
, 将ah设置为0
将dl设置为设备编号, 然后int 0x13
对于ah=0的int 0x13调用我已经很熟悉了, 就是重置设备dl
最后一句jmp short 0x59
就重新回去执行上面那个尝试读取过程了
意思就是重置一次设备并重试读取
那么如果读取成功了呢?
插一句话: 看到Notepad++的滚动条愈发向下, 笔者更有信心了
000000BB 813EFE7D55AA cmp word [0x7dfe],0xaa55
000000C1 756E jnz 0x131
000000C3 FF7600 push word [bp+0x0]
000000C6 E88D00 call 0x156
000000C9 7517 jnz 0xe2
这段代码检查上面读入的引导扇区是否以0xaa55结尾, 也就是是否有效扇区
不是则跳转到0x131
0x131会干嘛呢?
00000131 A0B707 mov al,[0x7b7]
00000134 EB08 jmp short 0x13e
输出0x7b7的文字, 但我一脸懵逼, 那里根本没有文字, 然后调用0x156, 后面讲
000000CB FA cli
000000CC B0D1 mov al,0xd1
000000CE E664 out 0x64,al
000000D0 E88300 call 0x156
000000D3 B0DF mov al,0xdf
000000D5 E660 out 0x60,al
000000D7 E87C00 call 0x156
000000DA B0FF mov al,0xff
000000DC E664 out 0x64,al
000000DE E87500 call 0x156
000000E1 FB sti
在执行这段代码时, 首尾cli
和sti
说明中途需要关闭中断, 否则会有问题
我一开始以为是进入保护模式, 发现原来不是, 只是打开A20地址线, 但是我见过的书都没有这么长的代码, 到Wiki搜索之后才发现是A20地址线, Wiki上说, 有些BIOS默认打开A20, 但Windows并没有测试A20是否已打开, 直接尝试打开
Wiki给出这样的代码用于打开A20:
enable_A20:
cli
call a20wait
mov al,0xAD
out 0x64,al
call a20wait
mov al,0xD0
out 0x64,al
call a20wait2
in al,0x60
push eax
call a20wait
mov al,0xD1
out 0x64,al
call a20wait
pop eax
or al,2
out 0x60,al
call a20wait
mov al,0xAE
out 0x64,al
call a20wait
sti
ret
a20wait:
in al,0x64
test al,2
jnz a20wait
ret
a20wait2:
in al,0x64
test al,1
jz a20wait2
ret
哎这其实很像Windows的代码啊!
Windows的代码有很多call 0x156
啊?
看看这里的代码:
00000156 2BC9 sub cx,cx
00000158 E464 in al,0x64
0000015A EB00 jmp short 0x15c
0000015C 2402 and al,0x2
0000015E E0F8 loopne 0x158
00000160 2402 and al,0x2
00000162 C3 ret
sub cx, cx
将cx减去cx??可能是置0吧
in al, 0x64
和Wiki一样, 后面的就不一样了, 可是...jmp short 0x15c
有什么用?我也不知道, and al,0x2
是位与, 然后loopne 0x158
, 只要cx != 0就跳转
好乱啊, 总之这段代码打开A20就行了, 好乱!
对了, 后面最后那个and是前面用的, 在000000C9有用, 000000C9根据判断会可选的跳过打开A20, 所以应该是判断是否BIOS自动打开吧.
随后我二脸懵逼:
000000E2 B800BB mov ax,0xbb00
000000E5 CD1A int 0x1a
在http://www.ctyme.com/intr/int-1a.htm没有任何记载, 哎!
000000E7 6623C0 and eax,eax
000000EA 753B jnz 0x127
000000EC 6681FB54435041 cmp ebx,0x41504354
000000F3 7532 jnz 0x127
000000F5 81F90201 cmp cx,0x102
000000F9 722C jc 0x127
也就是eax != 0则跳到0x127, ebx == 0x41504354 则跳0x127, 最后一个跳转我百思不得其解, 因为我不知道cmp设置CF的条件
000000FB 666807BB0000 push dword 0xbb07
00000101 666800020000 push dword 0x200
00000107 666808000000 push dword 0x8
0000010D 6653 push ebx
0000010F 6653 push ebx
00000111 6655 push ebp
00000113 666800000000 push dword 0x0
00000119 6668007C0000 push dword 0x7c00
0000011F 6661 popad
上面的一堆push模拟了一个pushad操作, 然后popad, 其中, ebx和ebp没有更改, esp变成ebx
pushad的顺序:(E)AX, (E)CX, (E)DX, (E)BX, (E)SP, (E)BP, (E)SI, (E)DI
所以相当于下面代码:
mov eax, 0xbb07
mov ecx, 0x200
mov edx, 0x8
mov esp, ebx
mov esi, 0x0
mov edi, 0x7c00
然后还是设置:
00000121 680000 push word 0x0
00000124 07 pop es
设置es为0x0
然后:
00000125 CD1A int 0x1a
上面设置了eax为0xbb07, 隐含mov ax, 0x07
mov ax, 0x07
等于mov al, 0x07
真不明白, TIME - SET ALARM (AT,XT286,PS)
是Int 1A/AH=06h, 可是从未调用过
这个int 0x1a
定义在http://www.ctyme.com/intr/rb-2283.htm
取消时间报警?
00000127 5A pop dx
00000128 32F6 xor dh,dh
0000012A EA007C0000 jmp 0x0:0x7c00
0000012F CD18 int 0x18
pop dx
又是什么操作, 然后dh置0, 所以只取dl?
好吧是取引导设备号, 在上面push的
000000C3 FF7600 push word [bp+0x0]
一句jmp
执行加载的引导扇区
0000012A EA007C0000 jmp 0x0:0x7c00
顺便补充下上一篇的问题, int 0x18是中断启动, 一旦int 0x18, 不会继续引导!
一般设备int 0x18会显示"No bootable device"等
总结
Windows的硬盘引导扇区其实是一个Loader, 寻找活动分区, 加载该分区第一个扇区并执行, 所以真正的代码应该在引导分区的第一个扇区, 好吧我承认这次找错了