x86汇编语言-从实模式到保护模式笔记

硬盘和显卡的访问与控制

Posted by BINBIN Blog on October 23, 2019

主引导扇区过后是什么

主引导扇区是处理器迈向广阔天地的第一块跳板。离开主引导扇区后,前方通常就是操作系统。

和主引导扇区一样,操作系统也是位于硬盘上的。操作系统需要安装到硬盘上。这个安装的过程不仅需要将操作系统的指令和数据写入硬盘,通常还要更新主引导扇区的内容。好让主引导扇区直接连着操作系统。

我们前面写的主引导扇区一直都是在显示字符串和做加法。这这太过简单。不过作为初学,很有必要。

操作系统通常肩负着处理器管理、内存分配、程序加载、进程调度等的任务。想要自己写一个操作系统,还是相当困难的。但是我们可以模拟一下操作系统的一些功能,写一个小程序。比如,我们可以模拟操作系统加载用户程序到内存的这一过程。

我们知道编译好的程序通常都是存放在硬盘这样的载体上,需要加载到内存之后才能运行。这个过程很复杂。首先需要读取硬盘,然后决定把它加载到内存的什么位置。最重要的是程序通常都是分段的,载入内存之后,还需要重新计算它的段地址。这叫做段的重定位。

那么本篇文章目的就是:把主引导扇区改造成一个加载器程序,它的功能是加载用户程序,并执行该程序(将处理器的控制权交给用户程序)。

代码清单

8-2(被加载的用户程序),源程序文件:c08.asm

1  			 ;代码清单8-2
2  			 ;文件名:c08.asm
3  			 ;文件说明:用户程序 
4  			 ;创建日期:2011-5-5 18:17
5  			 
6  	;===============================================================================
7  	SECTION header vstart=0                     ;定义用户程序头部段 
8  		program_length  dd program_end          ;程序总长度[0x00]
9  		
10 		;用户程序入口点
11 		code_entry      dw start                ;偏移地址[0x04]
12 						dd section.code_1.start ;段地址[0x06] 
13 		
14 		realloc_tbl_len dw (header_end-code_1_segment)/4
15 												;段重定位表项个数[0x0a]
16 		
17 		;段重定位表           
18 		code_1_segment  dd section.code_1.start ;[0x0c]
19 		code_2_segment  dd section.code_2.start ;[0x10]
20 		data_1_segment  dd section.data_1.start ;[0x14]
21 		data_2_segment  dd section.data_2.start ;[0x18]
22 		stack_segment   dd section.stack.start  ;[0x1c]
23 		
24 		header_end:                
25 		
26 	;===============================================================================
27 	SECTION code_1 align=16 vstart=0         ;定义代码段1(16字节对齐) 
28 	put_string:                              ;显示串(0结尾)。
29 											 ;输入:DS:BX=串地址
30 			 mov cl,[bx]
31 			 or cl,cl                        ;cl=0 ?
32 			 jz .exit                        ;是的,返回主程序 
33 			 call put_char
34 			 inc bx                          ;下一个字符 
35 			 jmp put_string
36 
37 	   .exit:
38 			 ret
39 
40 	;-------------------------------------------------------------------------------
41 	put_char:                                ;显示一个字符
42 											 ;输入:cl=字符ascii
43 			 push ax
44 			 push bx
45 			 push cx
46 			 push dx
47 			 push ds
48 			 push es
49 
50 			 ;以下取当前光标位置
51 			 mov dx,0x3d4
52 			 mov al,0x0e
53 			 out dx,al
54 			 mov dx,0x3d5
55 			 in al,dx                        ;高8位 
56 			 mov ah,al
57 
58 			 mov dx,0x3d4
59 			 mov al,0x0f
60 			 out dx,al
61 			 mov dx,0x3d5
62 			 in al,dx                        ;低8位 
63 			 mov bx,ax                       ;BX=代表光标位置的16位数
64 
65 			 cmp cl,0x0d                     ;回车符?
66 			 jnz .put_0a                     ;不是。看看是不是换行等字符 
67 			 mov ax,bx                       ;此句略显多余,但去掉后还得改书,麻烦 
68 			 mov bl,80                       
69 			 div bl
70 			 mul bl
71 			 mov bx,ax
72 			 jmp .set_cursor
73 
74 	 .put_0a:
75 			 cmp cl,0x0a                     ;换行符?
76 			 jnz .put_other                  ;不是,那就正常显示字符 
77 			 add bx,80
78 			 jmp .roll_screen
79 
80 	 .put_other:                             ;正常显示字符
81 			 mov ax,0xb800
82 			 mov es,ax
83 			 shl bx,1
84 			 mov [es:bx],cl
85 
86 			 ;以下将光标位置推进一个字符
87 			 shr bx,1
88 			 add bx,1
89 
90 	 .roll_screen:
91 			 cmp bx,2000                     ;光标超出屏幕?滚屏
92 			 jl .set_cursor
93 
94 			 mov ax,0xb800
95 			 mov ds,ax
96 			 mov es,ax
97 			 cld
98 			 mov si,0xa0
99 			 mov di,0x00
100			 mov cx,1920
101			 rep movsw
102			 mov bx,3840                     ;清除屏幕最底一行
103			 mov cx,80
104	 .cls:
105			 mov word[es:bx],0x0720
106			 add bx,2
107			 loop .cls
108
109			 mov bx,1920
110
111	 .set_cursor:
112			 mov dx,0x3d4
113			 mov al,0x0e
114			 out dx,al
115			 mov dx,0x3d5
116			 mov al,bh
117			 out dx,al
118			 mov dx,0x3d4
119			 mov al,0x0f
120			 out dx,al
121			 mov dx,0x3d5
122			 mov al,bl
123			 out dx,al
124
125			 pop es
126			 pop ds
127			 pop dx
128			 pop cx
129			 pop bx
130			 pop ax
131
132			 ret
133
134	;-------------------------------------------------------------------------------
135	  start:
136			 ;初始执行时,DS和ES指向用户程序头部段
137			 mov ax,[stack_segment]           ;设置到用户程序自己的堆栈 
138			 mov ss,ax
139			 mov sp,stack_end
140			 
141			 mov ax,[data_1_segment]          ;设置到用户程序自己的数据段
142			 mov ds,ax
143
144			 mov bx,msg0
145			 call put_string                  ;显示第一段信息 
146
147			 push word [es:code_2_segment]
148			 mov ax,begin
149			 push ax                          ;可以直接push begin,80386+
150			 
151			 retf                             ;转移到代码段2执行 
152			 
153	  continue:
154			 mov ax,[es:data_2_segment]       ;段寄存器DS切换到数据段2 
155			 mov ds,ax
156			 
157			 mov bx,msg1
158			 call put_string                  ;显示第二段信息 
159
160			 jmp $ 
161
162	;===============================================================================
163	SECTION code_2 align=16 vstart=0          ;定义代码段2(16字节对齐)
164
165	  begin:
166			 push word [es:code_1_segment]
167			 mov ax,continue
168			 push ax                          ;可以直接push continue,80386+
169			 
170			 retf                             ;转移到代码段1接着执行 
171			 
172	;===============================================================================
173	SECTION data_1 align=16 vstart=0
174
175		msg0 db '  This is NASM - the famous Netwide Assembler. '
176			 db 'Back at SourceForge and in intensive development! '
177			 db 'Get the current versions from http://www.nasm.us/.'
178			 db 0x0d,0x0a,0x0d,0x0a
179			 db '  Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
180			 db '     xor dx,dx',0x0d,0x0a
181			 db '     xor ax,ax',0x0d,0x0a
182			 db '     xor cx,cx',0x0d,0x0a
183			 db '  @@:',0x0d,0x0a
184			 db '     inc cx',0x0d,0x0a
185			 db '     add ax,cx',0x0d,0x0a
186			 db '     adc dx,0',0x0d,0x0a
187			 db '     inc cx',0x0d,0x0a
188			 db '     cmp cx,1000',0x0d,0x0a
189			 db '     jle @@',0x0d,0x0a
190			 db '     ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
191			 db 0
192
193	;===============================================================================
194	SECTION data_2 align=16 vstart=0
195
196		msg1 db '  The above contents is written by LeeChung. '
197			 db '2011-05-06'
198			 db 0
199
200	;===============================================================================
201	SECTION stack align=16 vstart=0
202			   
203			 resb 256
204
205	stack_end:  
206
207	;===============================================================================
208	SECTION trail align=16
209	program_end:



8-1 (主引导扇区程序/加载器),源程序文件:c08_mbr.asm

1  			 ;代码清单8-1
2  			 ;文件名:c08_mbr.asm
3  			 ;文件说明:硬盘主引导扇区代码(加载程序) 
4  			 ;创建日期:2011-5-5 18:17
5  			 
6  			 app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号)
7  											 ;常数的声明不会占用汇编地址
8  										
9  	SECTION mbr align=16 vstart=0x7c00                                     
10 
11 			 ;设置堆栈段和栈指针 
12 			 mov ax,0      
13 			 mov ss,ax
14 			 mov sp,ax
15 		  
16 			 mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 
17 			 mov dx,[cs:phy_base+0x02]
18 			 mov bx,16        
19 			 div bx            
20 			 mov ds,ax                       ;令DS和ES指向该段以进行操作
21 			 mov es,ax                        
22 		
23 			 ;以下读取程序的起始部分 
24 			 xor di,di
25 			 mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 
26 			 xor bx,bx                       ;加载到DS:0x0000处 
27 			 call read_hard_disk_0
28 		  
29 			 ;以下判断整个程序有多大
30 			 mov dx,[2]                      ;曾经把dx写成了ds,花了二十分钟排错 
31 			 mov ax,[0]
32 			 mov bx,512                      ;512字节每扇区
33 			 div bx
34 			 cmp dx,0
35 			 jnz @1                          ;未除尽,因此结果比实际扇区数少1 
36 			 dec ax                          ;已经读了一个扇区,扇区总数减1 
37 	   @1:
38 			 cmp ax,0                        ;考虑实际长度小于等于512个字节的情况 
39 			 jz direct
40 			 
41 			 ;读取剩余的扇区
42 			 push ds                         ;以下要用到并改变DS寄存器 
43 
44 			 mov cx,ax                       ;循环次数(剩余扇区数)
45 	   @2:
46 			 mov ax,ds
47 			 add ax,0x20                     ;得到下一个以512字节为边界的段地址
48 			 mov ds,ax  
49 								  
50 			 xor bx,bx                       ;每次读时,偏移地址始终为0x0000 
51 			 inc si                          ;下一个逻辑扇区 
52 			 call read_hard_disk_0
53 			 loop @2                         ;循环读,直到读完整个功能程序 
54 
55 			 pop ds                          ;恢复数据段基址到用户程序头部段 
56 		  
57 			 ;计算入口点代码段基址 
58 	   direct:
59 			 mov dx,[0x08]
60 			 mov ax,[0x06]
61 			 call calc_segment_base
62 			 mov [0x06],ax                   ;回填修正后的入口点代码段基址 
63 		  
64 			 ;开始处理段重定位表
65 			 mov cx,[0x0a]                   ;需要重定位的项目数量
66 			 mov bx,0x0c                     ;重定位表首地址
67 			  
68 	 realloc:
69 			 mov dx,[bx+0x02]                ;32位地址的高16位 
70 			 mov ax,[bx]
71 			 call calc_segment_base
72 			 mov [bx],ax                     ;回填段的基址
73 			 add bx,4                        ;下一个重定位项(每项占4个字节) 
74 			 loop realloc 
75 		  
76 			 jmp far [0x04]                  ;转移到用户程序  
77 	 
78 	;-------------------------------------------------------------------------------
79 	read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
80 											 ;输入:DI:SI=起始逻辑扇区号
81 											 ;      DS:BX=目标缓冲区地址
82 			 push ax
83 			 push bx
84 			 push cx
85 			 push dx
86 		  
87 			 mov dx,0x1f2
88 			 mov al,1
89 			 out dx,al                       ;读取的扇区数
90 
91 			 inc dx                          ;0x1f3
92 			 mov ax,si
93 			 out dx,al                       ;LBA地址7~0
94 
95 			 inc dx                          ;0x1f4
96 			 mov al,ah
97 			 out dx,al                       ;LBA地址15~8
98 
99 			 inc dx                          ;0x1f5
100			 mov ax,di
101			 out dx,al                       ;LBA地址23~16
102
103			 inc dx                          ;0x1f6
104			 mov al,0xe0                     ;LBA28模式,主盘
105			 or al,ah                        ;LBA地址27~24
106			 out dx,al
107
108			 inc dx                          ;0x1f7
109			 mov al,0x20                     ;读命令
110			 out dx,al
111
112	  .waits:
113			 in al,dx
114			 and al,0x88
115			 cmp al,0x08
116			 jnz .waits                      ;不忙,且硬盘已准备好数据传输 
117
118			 mov cx,256                      ;总共要读取的字数
119			 mov dx,0x1f0
120	  .readw:
121			 in ax,dx
122			 mov [bx],ax
123			 add bx,2
124			 loop .readw
125
126			 pop dx
127			 pop cx
128			 pop bx
129			 pop ax
130		  
131			 ret
132
133	;-------------------------------------------------------------------------------
134	calc_segment_base:                       ;计算16位段地址
135											 ;输入:DX:AX=32位物理地址
136											 ;返回:AX=16位段基地址 
137			 push dx                          
138			 
139			 add ax,[cs:phy_base]
140			 adc dx,[cs:phy_base+0x02]
141			 shr ax,4
142			 ror dx,4
143			 and dx,0xf000
144			 or ax,dx
145			 
146			 pop dx
147			 
148			 ret
149
150	;-------------------------------------------------------------------------------
151			 phy_base dd 0x10000             ;用户程序被加载的物理起始地址
152			 
153	 times 510-($-$$) db 0
154					  db 0x55,0xaa

用户程序的结构

处理器的工作模式是将内存分成逻辑上的段,指令的获取和数据的访问一律按“段地址:偏移地址”的方式进行。相对应地,一个规范的程序,应当包括代码段、数据段、附加段和堆栈段。这样一来,段的划分和段与段之间的界限在程序加载到内存之前就已经准备好了。

为了清楚起见,图 8-1 给出了整个源程序的组织结构。

NASM 编译器使用汇编指令“SECTION”或者“SEGMENT”来定义段。它的一般格式是:

SECTION 段名称

或者

SEGMENT 段名称

每个段都要求给出名字,这就是段名称,它主要用来引用一个段,可以是任意名字,只要它们彼此之间不会重复和混淆。

起个直观易懂的名字很有必要的!如图 8-1 所示,第一个段的名字是“header”,表明它是整个程序的开头部分;第二个段的名字是“code”,表明这是代码段;第三个段的名字是“data”,表明这是数据段。

一旦定义段,那么,后面的内容就都属于该段,除非又出现了另一个段的定义。

如图 8-2 所示,有时候,程序并不以段定义语句开始。在这种情况下,这些内容默认地自成一个段。最为典型的情况是,整个程序中都没有段定义语句。这时,整个程序自成一个段。

NASM 对段的数量没有限制。一些大的程序,可能拥有不止一个代码段和数据段。 Intel 处理器要求段在内存中的起始物理地址起码是 16 字节对齐的。这句话的意思是,必须是16 的倍数,或者说该物理地址必须能被 16 整除!

相应地,汇编语言源程序中定义的各个段,也有对齐方面的要求。具体做法是,在段定义中使用“align=”子句,用于指定某个 SECTION 的汇编地址对齐方式。比如说,“align=16”就表示段是 16 字节对齐的,“align=32”就表示段是 32 字节对齐的。

在源程序编译阶段,编译器将根据 align 子句确定段的起始汇编地址。如图 8-3 所示,这里定义了三个段,分别是 data1、 data2 和 data3,每个段里只有一个字节的数据,分别是 0x55、0xaa 和 0x99。

理论上,如果不考虑段的对齐方式,那么段 data1 的汇编地址是 0, 段 data2 的汇编地址是 1,段 data3 的汇编地址是 2。

但是,在这里,每个段的定义中都包含了要求 16 字节对齐的子句,情况便不同了。如图 8-4所示,这是编译后的结果,因为在段 data1 之前没有任何内容,故段 data1 的起始汇编地址是 0(在图中是 0x00000000),而且地址 0 本身就是 16 字节对齐的,符合 align 子句的要求。 段的汇编地址其实就是段内第一个元素(数据、指令)的汇编地址。因此,在段 data1 中声明和初始化的 0x55 位于汇编地址 0x00000000 处。 段 data2 也要求是 16 字节对齐的。问题是,从汇编地址 0x00000001 开始,只有 0x00000010(十进制的 16)才能被 16 整除。于是,编译器将 0x00000010 作为段 data2 的汇编地址,并在两个段之间填充 15 字节的 0x00(段 data1 只有 1 字节的长度)。 段 data3 的处理与前面两个段相同。因为段 data2 只有 1 字节,故也需要在它们之间填充 15 字节。这样,段 data3 的汇编地址就是 0x00000020(十进制的 32)。段 data3 也只有 1 字节(0x99), 所以,汇编地址 0x00000020 处是 0x99,这也是编译结果中的最后一字节。

每个段都有一个汇编地址,它是相对于整个程序开头(0)的。为了 方便取得该段的汇编地址, NASM 编译器提供了以下的表达式,可以用在你的程序中:

section.段名称.start

段“header”相对于整个程序开头的汇编地址是 ection.header.start,段“code”相对于整个程序开头的汇编地址是 section.code.start。在这个例子中,因为段“header”是在程序的一开始定义的,它的前面没有其他内容,故 section.header.start=0。

注意:段定义语句还可以包含“vstart=”子句。尽管定义了段,但是,引用某个标号时,该标号处的汇编地址依然是从整个程序的开头计算的,而不是从段的开头处计算的!因为有 vstart=0 这个句子! 可以打开c08.list,显示汇编地址:

最后一个段 trail 的定义中没有包含“vstart=0”子句。 那就对不起了,该段内有一个标号“program_end”,它的汇编地址就要从整个程序开头计算

用户程序头部

本章的用户程序实际上定义了 7 个段,分别是第 7行定义的段 header、第 27 行定义的段 code_1、第 163 行定义的段 code_2、第 173 行定义的段 data_1、第 194 行定义的段 data_2、第 201 行定义的段 stack 和第 208 行定义的段 trail。

一般情况下,加载器程序和用户程序是由不同的公司不同的人开发的。所以加载器与用户程序实际上彼此并不知道彼此长什么样。他们并不了解彼此的结构与功能。

那么加载器该如何加载用户程序呢?

首先用户程序中必须得有一些信息,加载器可以利用这些信息将用户程序加载到内存中去。

实际上在用户程序中,有这么一个段,叫做头部。它里面包含了一些重要的信息,加载器利用这些信息足以将用户程序加载到内存中进行运行。顾名思义,头部,在用户程序的开头位置。如下图:

头部需要在源程序以一个段的形式出现。这就是代码清单 8-2 的第 7 行: ` SECTION header vstart=0 ` 而且,因为它是“头部”,所以, 该段当然必须是第一个被定义的段,且总是位于整个源程序的开头。

用户程序为了能够让加载器将自己加载到内存中去,必须包含以下介个方面:

  • 用户程序的尺寸,即以字节为单位的大小! 代码清单 8-2 中第 8 行,伪指令 dd 用于声明和初始化一个双字,即一个 32 位的数据。用户程序可能很大, 16 位的长度不足以表示 65535 以上的数值。program_end 所代表的汇编地址,在数值上等于整个程序的长度。
  • 应用程序的入口点,包括段地址和偏移地址!
  • 理想情况下,当用户程序开始运行时,执行的第一条指令是其代码段内的第一条指令。换句话说,入口点位于其代码段内偏移地址为 0 的地方。但是,情况并非总是如此。尤其是,很多程序并非只有一个代码段,比如本章源代码清单 8-2 就包含了两个代码段。所以,需要在用户程序头部明确给出用户程序在刚开始运行时,第一条指令的位置,也就是第一条指令在用户 程序代码段内的偏移地址。 代码清单 8-2 第 11、 12 行,依次声明并初始化了入口点的偏移地址和段地址。偏移地址取自代码段 code_1 中的标号“start”,段地址是用表达式 section.code_1.start 得到的。 代码段 code_1 是在代码清单 8-2 的第 27 行定义的: ` SECTION code_1 align=16 vstart=0 ` 显而易见的是,因为段定义中包含了“vstart=0”子句,故标号 start 所代表的汇编地址是相对于当前代码段 code_1 的起始位置,从 0 开始计算的。 入口点的段地址是用伪指令 dd 声明的,并初始化为汇编地址 section.code_1.start,这是一个 32 位的地址。不过,它仅仅是编译阶段确定的汇编地址,在用户程序加载到内存后,需要根据加载的实际位置重新计算(浮动)

  • 段重定位表以及每个表的表项。因为用户程序一般不止一个段,比较大的程序可能包含多个代码段和数据段。在程序没有加载进内存之前,各个段都有自己的段地址(即汇编地址),但是程序加载进内存之后,一般来说加载到哪个内存地址是不知道的,所以说此时各个段的实际内存地址肯定变了,所以此时需要对段进行重定位。

段的重定位是加载器的工作,它需要知道每个段在用户程序内的位置,即它们分别位于用户程序内的多少字节处。为此,需要在用户程序头部建立一张段重定位表。

用户程序可以定义的段在数量上是不确定的,因此,段重定位表的大小,或者说表项数是不确定的。为此,代码清单 8-2 第 14 行,声明并初始化了段重定位表的项目数。因为段重定位表位于两个标号 header_end 和 code_1_segment 之间,而且每个表项占用 4 字节,故实际的表项数为

(header_end – code_1_segment) / 4

这个值是在程序编译阶段计算的,先用两个标号所代表的汇编地址相减,再除以每个表项的长度 4。

紧接着表项数的,是实际的段重定位表,每个表项用伪指令 dd 声明并初始化为 1 个双字。代码清单 8-2 一共定义了 5 个段,所以这里有 5 个表项,依次计算段开始汇编地址的表达式并进行初始化。

加载器的工作流程

从大的方面来说,加载器要加载一个用户程序,并使之开始执行,需要决定两件事。第一,看看内存中的什么地方是空闲的,即从哪个物理内存地址开始加载用户程序;第二,用户程序位于硬盘上的什么位置,它的起始逻辑扇区号是多少。如果你连它在哪里都不知道,怎么找得到它呢! 现在,让我们把目光转移到代码清单 8-1,来看看加载器都做了哪些工作。 代码清单 8-1 第 6 行,加载器程序的一开始声明了一个常数(const):

app_lba_start equ 100

常数是用伪指令 equ 声明的,它的意思是“等于”。本语句的意思是,用标号 app_lba_start 来代表数值 100! 就是C 语言的宏定义!

加载用户程序需要确定一个内存物理地址,这是在代码清单 8-1 第 151 行用伪指令 dd 声明的,并初始化为 0x10000 的。和前面一样,是用 32 位的单元来容纳一个 20 位的地址:

phy_base dd 0x10000

尽管我们用了一个好看的数 0x10000,但你完全可以把用户程序加载到其他地方,只要它是空闲的。可以将这个数值改成 0x12340,唯一的要求是 该地址的最低 4 位必须是 0,换句话说,加载的起始地址必须是 16 字节对齐的! 图中的是我们可以在0x10000-0x9FFFF范围内加载用户程序。差不多500多KB!

准备加载用户程序

以往不同,我们将主引导扇区程序定义成一个段。代码清单 8-1 第 9 行:

SECTION mbr align=16 vstart=0x7c00

整个程序只定义了这一个段,所以它略显多余。之所以这么说,是因为,即使你不定义这个段,编译器也会自动把整个程序看成一个段!

但是,因为该定义中有“vstart=0x7c00”子句,所以,它就不那么多余了。一旦有了该子句,段内所有元素的汇编地址都将从 0x7c00 开始计算。否则,因为主引导程序的实际加载地址是0x0000:0x7c00,当我们引用一个标号时,还得手工加上那个落差 0x7c00。

代码清单 8-1 第 12~14 行,用于初始化堆栈段寄存器 SS 和堆栈指针 SP。之后,堆栈的段地址是 0x0000,段的长度是 64KB,堆栈指针将在段内 0xFFFF 和 0x0000 之间变化。

代码清单 8-1 第 16、 17 行,用于取得一个地址,用户程序将要从这个地址处开始加载。

该地址实际上是保存在标号 phy_base 处的一个双字单元里。这是一个 32 位的数,在 16 位的处理器上,只能用两个寄存器存放。如图 8-8 所示, 32 位数内存中的存放是按低端序列的,高 16 位处在 phy_base+0x02 处,可以放在寄存器 DX 中;低 16 位处在 phy_base 处,可以用寄存器 AX 存放。


mov ax,[cs:phy_base]
mov dx,[cs:phy_base+0x02]

代码清单 8-1 第 18~21 行,用于将该物理地址变成 16 位的段地址,并传送到 DS 和 ES 寄存器。因为该物理地址是 16 字节对齐的,直接右移 4 位即可。实际上,右移 4 位相当于除以16(0x10),所以程序中的做法将这个 32 位物理地址(DX:AX)除以 16(在寄存器 BX 中),寄存 器 AX 中的商就是得到的段地址(在本程序中是 0x1000)。

好了到目前为止。加载器已经准备好了一个状态。这个状态是它已经取得了用户程序的加载地址(真实的物理地址),并且用DS于ES来指向这个个地址的段地址,以方便后期的操作。

那么接下来,就是读取硬盘上的用户程序了。说白了就是访问其他硬件。那么我们对如何访问硬盘以及从硬盘上读取数据并不感兴趣。所以这里直接略过这部分的汇编代码的详细解说(感兴趣的话可以阅读原书籍内容)。

但是有一点内容可以说明,就是从硬盘上读数据不是一下子就能读完的。所以在这里,设置了一个过程调用,在需要读数据的时候直接调用相关的读书的汇编代码即可,不需要重复写读数据的代码。

处理器支持过程调用的指令机制。过程实际上就是一段普通的代码。处理器可以用过程调用指令转移到这段代码执行,然后再遇到过程返回指令时重新返回到调用出的下一条指令接着执行。

如下图是一个过程调用示意图:

在调用其他过程之前,由于其他过程可能会使用一些寄存器,所以在这之前需要将这些寄存器的值先存起来,一般使用栈来保存这些值。在调用过程之后,再使用pop指令将之前保存过的寄存器的值在弹出来。

一般过程调用的指令是call指令。例如代码清单8-1中的24-27行就是用于读取程序的起始部分。

         xor di,di
         mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 
         xor bx,bx                       ;加载到DS:0x0000处 
         call read_hard_disk_0

在read_hard_disk_0这个标号下的代码,首先需要push一些寄存器:


79 	read_hard_disk_0:                        ;从硬盘读取一个逻辑扇区
80 											 ;输入:DI:SI=起始逻辑扇区号
81 											 ;      DS:BX=目标缓冲区地址
82 			 push ax
83 			 push bx
84 			 push cx
85 			 push dx

在最后的时候,将这些寄存器恢复:

126			 pop dx
127			 pop cx
128			 pop bx
129			 pop ax
130		  
131			 ret

如下图所示是调用前后栈的变化:

在call read_hard_disk_0指令执行前,栈指针位于箭头1 所指示的位置;call指令执行后,由于压入了IP的内容,故栈指针移动到箭头2 所指示的位置处;进入过程后,出于保护现场的目的,压入了4个通用寄存器AX,BX,CX,DX,此时栈指针继续向低地址方向推进到箭头3 所指示的位置。

在过程的最后,是恢复现场,连续反序弹出4个通用寄存器内容。此时栈指针又回到进入过程内部的位置,即箭头2 处。最后,ret指令执行时,由于处理器自动弹出一个字到IP寄存器,故过程返回后的瞬间,栈指针仍旧回到过程调用前,即箭头1 所指示的位置。然后处理器就继续之前的代码进行执行。

再回到上一段代码的意思,它是读程序的开始的一部分。

为什么要先读取程序的开始一部分呢(实际上是一个扇区512字节的大小)。因为这里面包含了程序的头部。加载器需要先将头部读进来,然后才能判断整个源程序的大小(防止多读或者少读),从而接着读剩下的代码。

代码清单8-2,30-55行,首先根据刚刚读的程序头,来计算用户程序的总长度。然后将整个程序代码加载进内存当中。这里的代码我就不贴了,可以自己看源码。下面我们先来看一下程序头部的各个条目在内存中目前的地址(偏移地址)。如下图:

由图中可知用户程序的总长度位于最开始的偏移地址为0的地方。并占有两个字。由此对比30-55行代码,将会更加清晰明了。

好了,整个用户程序已经被加载器加载到内存中了。那么接下来要做的就是对整个用户程序进行重定位工作了。

重定位用户程序

整个用户程序已经被加载器加载到内存中了。那么接下来要做的就是对整个用户程序进行重定位工作了。

实际上就是确定每个段的段地址即可(并不需要知道每一条指令的地址)。重定位实际上就是在现在这个真实的物理内存上计算出各个段的段地址,然后将真实的段地址再覆盖程序头部的各个段原来的汇编段地址即可。

由于用户程序的各个段的汇编地址是可以得出来的,所以我们可以计算各个段的长度。知道了各个段的长度,然后又知道用户程序在内存中的起始位置地址phy_base。那么就可以很容易计算出各个段在内存中的地址。如下图,清晰明了:

以上图示清晰的展示了内存中的各个段与源程序的汇编地址的表示的段的关系。

源程序58-62行重定位了用户程序的入口点的代码段。

65-74行,重定位其他各个段。

将控制权交给用户程序

76行代码:jmp far [0x04] ;转移到用户程序

当对用户程序的各个段进行了重定位后。就将控制权交给用户程序了。我们在此前知道用户程序的头结构在内存中的结构如下:

由此得知用处程序的入口点地址存在于内存的0x04处。所以上面来一个段间远跳转,将执行流跳转到内存偏移地址为0x04处。从而开始整个用户程序的执行。

就提是先访问DS所指向的数据段,从偏移地址0x04处取出两个字,并分别传送到代码段寄存器CS与指令指针寄存器IP,以替代他们原先的内容。于是处理器就自行转移到指定的位置开始执行指令。

至此,我们已经将用户程序运行起来了。真是相当的不容易啊!!!