程序的加载和执行(一)

本文及之后的几篇博文是原书第13章的学习笔记。
本章主要是学习一个例子,对应的代码分为3个文件:

 ;代码清单13-1;文件名:c13_mbr.asm;文件说明:硬盘主引导扇区代码 ;代码清单13-2;文件名:c13_core.asm;文件说明:保护模式微型核心程序 ;代码清单13-3;文件名:c13.asm;文件说明:用户程序

因为代码比较长,完整的我就不贴了。有需要朋友的可以到http://download.csdn.net/detail/u013490896/9388139下载。

本章的例子清楚地说明了4个步骤:
1. 主引导程序开始执行
2. 主引导程序加载内核(其实这个内核太简陋了,只是为了说明原理,我们就这样叫吧),并转交控制权给内核
3. 内核加载用户程序,执行用户程序
4. 用户程序通过调用内核例程返回到内核

内核的结构

我把代码清单13-2的源文件精简了一下,以清晰表示内核的结构。

;代码清单13-2
;文件名:c13_core.asm
;文件说明:内核结构
;以下常量定义部分。内核的大部分内容都应当固定
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       ;核心程序总长度#00sys_routine_seg  dd section.sys_routine.start;系统公用例程段位置#04core_data_seg    dd section.core_data.start;核心数据段位置#08core_code_seg    dd section.core_code.start;核心代码段位置#0ccore_entry       dd start          ;核心代码段入口点#10dw core_code_seg_sel;===============================================================================
[bits 32]SECTION sys_routine vstart=0                ;系统公共例程代码段 ......SECTION core_data vstart=0                  ;系统核心的数据段......SECTION core_code vstart=0                  ;内核代码段
......    start:......
;===============================================================================
core_end:

首先,用EQU声明了一些常量,需要注意的是:EQU声明的常量不占用空间。
其次,是内核的头部;
最后,是公共例程代码段、内核数据段、内核代码段。

内核头部示意图如下:

注意,内核代码段的入口共6个字节,前4个字节是段内偏移地址(它来自标号start,以后会被传送到EIP),后2个字节是内核代码段的选择子(=0x38)。

当引导程序加载完内核,内核加载完用户程序之后,内存布局示意图如下(只是示意图,没有严格按照比例绘制)

内核的加载

1         ;代码清单13-1
2         ;文件名:c13_mbr.asm
3         ;文件说明:硬盘主引导扇区代码
4         ;创建日期:2011-10-28 22:35
5
6         core_base_address equ 0x00040000   ;常数,内核加载的起始内存地址
7         core_start_sector equ 0x00000001   ;常数,内核的起始逻辑扇区号
8
9          mov ax,cs
10         mov ss,ax
11         mov sp,0x7c00
12
13         ;计算GDT所在的逻辑段地址
14         mov eax,[cs:pgdt+0x7c00+0x02]      ;GDT的32位物理地址
15         xor edx,edx
16         mov ebx,16
17         div ebx                            ;分解成16位逻辑地址
18
19         mov ds,eax                         ;令DS指向该段以进行操作
20         mov ebx,edx                        ;段内起始偏移地址

第6、7行,作者定义了2个常量,分别是内核加载的起始物理内存地址(也不一定非要这个值,只要合理规划就行)和内核的起始逻辑扇区号(在写入镜像文件的时候,要和这个扇区号对应)。

9~11行,设置实模式的栈和栈指针。

14~17,像之前的程序一样,把GDT的物理地址分解为逻辑地址(段地址:偏移地址),于是 DS:EBX就指向了GDT的起始位置。

22         ;跳过0#号描述符的槽位
23         ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间
24         mov dword [ebx+0x08],0x0000ffff    ;基地址为0,段界限为0xFFFFF
25         mov dword [ebx+0x0c],0x00cf9200    ;粒度为4KB,存储器段描述符
26
27         ;创建保护模式下初始代码段描述符
28         mov dword [ebx+0x10],0x7c0001ff    ;基地址为0x00007c00,界限0x1FF
29         mov dword [ebx+0x14],0x00409800    ;粒度为1个字节,代码段描述符
30
31         ;建立保护模式下的堆栈段描述符      ;基地址为0x00007C00,界限0xFFFFE
32         mov dword [ebx+0x18],0x7c00fffe    ;粒度为4KB
33         mov dword [ebx+0x1c],0x00cf9600
34
35         ;建立保护模式下的显示缓冲区描述符
36         mov dword [ebx+0x20],0x80007fff    ;基地址为0x000B8000,界限0x07FFF
37         mov dword [ebx+0x24],0x0040920b    ;粒度为字节
38
39         ;初始化描述符表寄存器GDTR
40         mov word [cs: pgdt+0x7c00],39      ;描述符表的界限
41
42         lgdt [cs: pgdt+0x7c00]
43
44         in al,0x92                         ;南桥芯片内的端口
45         or al,0000_0010B
46         out 0x92,al                        ;打开A20
47
48         cli                                ;中断机制尚未工作
49
50         mov eax,cr0
51         or eax,1
52         mov cr0,eax                        ;设置PE位
53
54         ;以下进入保护模式... ...
55         jmp dword 0x0010:flush             ;16位的描述符选择子:32位偏移
56                                            ;清流水线并串行化处理器

24~37行,建立描述符,下图是GDT示意图。

在进入保护模式之后,首先设置DS和堆栈段,然后会加载内核的第一个扇区,因为第一个扇区包含了头部数据。

57         [bits 32]
58  flush:
59         mov eax,0x0008                     ;加载数据段(0..4GB)选择子
60         mov ds,eax
61
62         mov eax,0x0018                     ;加载堆栈段选择子
63         mov ss,eax
64         xor esp,esp                        ;堆栈指针 <- 0

于是DS指向了0~4GB的数据段;

66         ;以下加载系统核心程序
67         mov edi,core_base_address
68
69         mov eax,core_start_sector
70         mov ebx,edi                        ;起始地址
71         call read_hard_disk_0              ;以下读取程序的起始部分(一个扇区)
138 read_hard_disk_0:                       ;从硬盘读取一个逻辑扇区
139                                         ;EAX=逻辑扇区号
140                                         ;DS:EBX=目标缓冲区地址
141                                         ;返回:EBX=EBX+512

关于read_hard_disk_0这个过程代码我就不贴了,这个过程和原书第八章的代码类似。具体讲解可以参考我的博文:硬盘和显卡的访问与控制(二)——《x86汇编语言:从实模式到保护模式》读书笔记02
http://blog.csdn.net/longintchar/article/details/49454459
与第八章的那个读硬盘的过程相比,这个过程仅有几处不同:
1.用EAX传入28位的逻辑扇区号。
2.DS:EBX指向目标缓冲区的地址。
3.每次返回时,EBX会自增512.

因为DS指向0-4GB的数据段,所以67~71把内核的第一个扇区加载到了物理地址core_start_sector (=0x40000)处。如下图所示:

73         ;以下判断整个程序有多大
74         mov eax,[edi]                      ;核心程序尺寸
75         xor edx,edx
76         mov ecx,512                        ;512字节每扇区
77         div ecx
78
79         or edx,edx
80         jnz @1                             ;未除尽,因此结果比实际扇区数少1
81         dec eax                            ;已经读了一个扇区,扇区总数减1
82   @1:
83         or eax,eax                         ;考虑实际长度≤512个字节的情况
84         jz setup                           ;EAX=0 ?
85
86         ;读取剩余的扇区
87         mov ecx,eax                        ;32位模式下的LOOP使用ECX
88         mov eax,core_start_sector
89         inc eax                            ;从下一个逻辑扇区接着读
90   @2:
91         call read_hard_disk_0
92         inc eax
93         loop @2                            ;循环读,直到读完整个内核
94

上面这段代码首先判断程序的尺寸(保存在EAX中),然后做除法 EDX:EAX/512=EAX…EDX,根据商和余数读取剩余的扇区。计算原理与第八章的“代码清单8-1”中的代码类似。流程图可以参考我刚才提到的那篇博文。

需要特别提醒的是:83~84行的判断是必要的,不然的话,当剩余扇区数(EAX)为0时,循环将会执行(0xFFFF_FFFF+1)次,哦,这真是一个重大的BUG。

加载完内核后,我们要根据头部信息向GDT追加描述符。


95 setup:
96          mov esi,[0x7c00+pgdt+0x02]         ;不可以在代码段内寻址pgdt,但可以
97                                             ;通过4GB的段来访问
98         ;建立公用例程段描述符
99          mov eax,[edi+0x04]                 ;公用例程代码段起始汇编地址
100         mov ebx,[edi+0x08]                 ;核心数据段汇编地址
101         sub ebx,eax
102         dec ebx                            ;公用例程段界限
103         add eax,edi                        ;公用例程段基地址
104         mov ecx,0x00409800                 ;字节粒度的代码段描述符
105         call make_gdt_descriptor
106         mov [esi+0x28],eax
107         mov [esi+0x2c],edx
108
109         ;建立核心数据段描述符
110         mov eax,[edi+0x08]                 ;核心数据段起始汇编地址
111         mov ebx,[edi+0x0c]                 ;核心代码段汇编地址
112         sub ebx,eax
113         dec ebx                            ;核心数据段界限
114         add eax,edi                        ;核心数据段基地址
115         mov ecx,0x00409200                 ;字节粒度的数据段描述符
116         call make_gdt_descriptor
117         mov [esi+0x30],eax
118         mov [esi+0x34],edx
119
120         ;建立核心代码段描述符
121         mov eax,[edi+0x0c]                 ;核心代码段起始汇编地址
122         mov ebx,[edi+0x00]                 ;程序总长度
123         sub ebx,eax
124         dec ebx                            ;核心代码段界限
125         add eax,edi                        ;核心代码段基地址
126         mov ecx,0x00409800                 ;字节粒度的代码段描述符
127         call make_gdt_descriptor
128         mov [esi+0x38],eax
129         mov [esi+0x3c],edx
130
131         mov word [0x7c00+pgdt],63          ;描述符表的界限
132
133         lgdt [0x7c00+pgdt]

此时,整个的GDT示意图如下:

第98~107,是添加公共例程段描述符的具体代码。

98          ;建立公用例程段描述符
99          mov eax,[edi+0x04]                 ;公用例程代码段起始汇编地址
100         mov ebx,[edi+0x08]                 ;核心数据段汇编地址
101         sub ebx,eax
102         dec ebx                            ;公用例程段界限
103         add eax,edi                        ;公用例程段基地址
104         mov ecx,0x00409800                 ;字节粒度的代码段描述符
105         call make_gdt_descriptor
106         mov [esi+0x28],eax
107         mov [esi+0x2c],edx

第105行,调用了过程 call make_gdt_descriptor

195   make_gdt_descriptor:                  ;构造描述符
196                                         ;输入:EAX=线性基地址
197                                         ;      EBX=段界限
198                                         ;      ECX=属性(各属性位都在原始
199                                         ;      位置,其它没用到的位置清0)
200                                         ;返回:EDX:EAX=完整的描述符
201         mov edx,eax
202         shl eax,16
203         or ax,bx                        ;描述符前32位(EAX)构造完毕
204
205         and edx,0xffff0000              ;清除基地址中无关的位
206         rol edx,8
207         bswap edx                       ;装配基址的31~24和23~16  (80486+)
208
209         xor bx,bx
210         or edx,ebx                      ;装配段界限的高4位
211
212         or edx,ecx                      ;装配属性
213
214         ret

根据注释,这个过程的输入和返回都已经很清楚了,这个过程的功能是通过“段基地址(EAX),段限长(EBX),属性值(ECX)”这三个参数来构造一个描述符(EDX:EAX)。下面就具体讲解这个过程。

我们先复习一下段描述符的通用格式(图片选自赵炯的《Linux内核完全注释》)。

首先构造描述符的低32位(图片中下面的那个东东)。

201         mov edx,eax
202         shl eax,16
203         or ax,bx                        ;描述符前32位(EAX)构造完毕

201行,先备份一个EAX到EDX中,留在后面用。
202行,EAX左移16位,于是基地址的0-15位就位;
203行,段限长的0-15位就位;
于是描述符的低32位构造完毕。

接下来,构造描述符的高32位(图片中上面那个东东)。这个构造起来有点麻烦。
我们先学习一个指令——字节交换指令:bswap
在标准的32位处理器上,这个指令只允许32位的寄存器操作数,其格式为

bswap r32

处理器执行该指令时,按如下过程操作(DEST是指令中的操作数,TEMP是处理器内的一个临时寄存器)

是不是有些晕呢?没有关系,我绘制了一张图,这张图的特色是“渐变色”,很清楚地说明了字节是如何交换的。

看清楚了吧。
OK,我们继续。


205         and edx,0xffff0000              ;清除基地址中无关的位
206         rol edx,8
207         bswap edx                       ;装配基址的31~24和23~16  (80486+)
208
209         xor bx,bx
210         or edx,ebx                      ;装配段界限的高4位
211
212         or edx,ecx                      ;装配属性

以上代码是具体的构造过程,引用原书图13-6。

205~207三行执行完后,段基地址已经就位。
209行,清除段界限的15-0位,只保留19-16位。这里假设EBX寄存器的高12位全为0,其实安全的做法是把209行修改为

and ebx,0x000F_0000

210行,装配段界限到EDX寄存器。
212行,装配属性值到EDX寄存器。
至此,在EDX:EAX中得到了完整的64位的段描述符。

好了,现在再回到98~107行。代码再贴一次。

98          ;建立公用例程段描述符
99          mov eax,[edi+0x04]                 ;公用例程代码段起始汇编地址
100         mov ebx,[edi+0x08]                 ;核心数据段汇编地址
101         sub ebx,eax
102         dec ebx                            ;公用例程段界限
103         add eax,edi                        ;公用例程段基地址
104         mov ecx,0x00409800                 ;字节粒度的代码段描述符
105         call make_gdt_descriptor
106         mov [esi+0x28],eax
107         mov [esi+0x2c],edx

此时,DS:EDI仍然指向内核的起始位置。根据内核头部的构造,我们从头部取出公共例程代码段的起始汇编地址到EAX,再取出内核数据段的起始汇编地址到EBX,后者减去前者,就是公共例程段的长度,再减去一,就是段界限,EBX这个参数就准备好了。

然后准备参数EAX(段基址):因为公共例程段的起始汇编地址(已经传送到EAX了)是相对于内核的起始位置的,加载内核后,内核的起始位置在线性地址0x40000(就是EDI的值)处,所以,公共例程段的起始地址是(EAX+EDI),这就是103行的用意。

104行,填写属性值,注意要把无关的位都清零。
105行调用过程。
106~107利用返回值安装描述符。

其他描述符的构造和安装过程类似,这里从略。

跳转到内核入口点

最后一步,跳到内核的入口点开始执行内核程序。

135         jmp far [edi+0x10]

这是一个16位直接绝对远转移,在ds:[edi+0x10]处,是6字节的内核入口点。低32位是偏移地址,高16位是内核代码段的选择子。关于jmp指令,可以参考我的博文:
8086处理器的无条件转移指令——《x86汇编语言:从实模式到保护模式》读书笔记13
http://blog.csdn.net/longintchar/article/details/50529164

总结

啰嗦了这么多,不知道你是不是觉得什么都没有记住呢…
我们来总结一下引导程序的引导步骤吧。
1. 创建GDT

2. 令DS指向0-4GB数据段;初始化SS和ESP;
3. 调用read_hard_disk_0读取内核的第一个扇区到0x40000;
4. 判断内核的长度,根据长度再读取若干个扇区

5. ESI指向GDT的起始,调用过程make_gdt_descriptor向GDT追加关于内核的段描述符

6. 跳转到内核入口点

关于内核的执行,下篇博文我们再讨论。敬请期待……

程序的加载和执行(一)——《x86汇编语言:从实模式到保护模式》读书笔记21相关推荐

  1. 程序的加载和执行(六)——《x86汇编语言:从实模式到保护模式》读书笔记26

    程序的加载和执行(六)--<x86汇编语言:从实模式到保护模式>读书笔记26 通过本文能学到什么? NASM的条件汇编 用NASM编译的时候,通过命令行选项定义宏 Makefile的条件语 ...

  2. 程序的加载和执行(五)——《x86汇编语言:从实模式到保护模式》读书笔记25

    程序的加载和执行(五)--<x86汇编语言:从实模式到保护模式>读书笔记25 前面几篇博文终于把代码分析完了.这篇就来说说代码的编译.运行和调试. 1.代码的编译及写入镜像文件 之前我们都 ...

  3. 程序的加载和执行(四)——《x86汇编语言:从实模式到保护模式》读书笔记24

    程序的加载和执行(四)--<x86汇编语言:从实模式到保护模式>读书笔记24 通过本文能学到什么? 怎样跳转到用户程序 用户程序通过调用内核过程完成自己的功能 怎样从用户程序返回到内核 接 ...

  4. 程序的加载和执行(三)——《x86汇编语言:从实模式到保护模式》读书笔记23

    程序的加载和执行(三)--读书笔记23 接着上次的内容说. 关于过程load_relocate_program的讲解还没有完,还差创建栈段描述符和重定位符号表. 1.分配栈空间与创建栈段描述符 462 ...

  5. html 执行外部js的函数,javascript – Chrome扩展程序:加载并执行外部脚本

    我无法在我的chrome扩展程序中加载和执行外部js-script.看起来和 this question一样,但我仍然无法弄清楚为什么它在我的情况下不起作用. 我的想法是,我希望在我的内容脚本中有一些 ...

  6. 小程序动画加载只执行一次的问题

    问题 最近, 想做个小程序的圆盘抽奖出来, 想要实现的效果是点击一次就旋转一次. 不过每次只有第一次点击有效, 再次点击就没有任何动画效果. 代码如下 rotate: function() {// 创 ...

  7. 任务和特权级保护(一)——《x86汇编语言:从实模式到保护模式》读书笔记27

    本文及后面的几篇文章是原书第14章的读书笔记. 1.LDT(局部描述符表) 在之前的学习中,不管是内核程序还是用户程序,我们都是把段描述符放在GDT中.但是,为了有效实施任务间的隔离,处理器建议每个任 ...

  8. X86汇编语言从实模式到保护模式13:保护模式程序的动态加载和执行

    目录 1. 引入保护模式对程序加载与执行的影响 1.1 对应用程序的影响 1.2 对操作系统的影响 1.3 本章程序总体结构 2. MBR加载内核过程分析 2.1 内核头部段分析 2.1.1 内核总长 ...

  9. Java的加载与执行原理详解 Java程序从编写到最终运行经历了哪些过程

    前言 Java程序从编写到最终运行大概可概括为3个阶段:编写.编译.运行阶段. 一.编写阶段 程序员在硬盘某个位置新建一个xxx.java文件 使用记事本或者其他文本编辑器例如EditPlus打开xx ...

最新文章

  1. 移动端手势库Hammer.js学习
  2. 鹅厂666,用梅花桩遛狗
  3. [攻防世界 pwn]——forgot
  4. 计划Java EE 7批处理作业
  5. 10分钟腾讯云配置免费https
  6. LeetCode 1516. Move Sub-Tree of N-Ary Tree(DFS)
  7. 好用的工具网站!(缓慢收集中!)
  8. 测试人必会:Python带你上手WebSocket
  9. TCP/IP模型各个层次的功能和协议
  10. PostgreSQL之Foreign Data Wrappers使用指南
  11. 9.2 多元微分学及应用——偏导数
  12. 证件照怎么裁剪?国考证件照的尺寸是多少?
  13. 天天打排位,你知道王者荣耀的皮肤怎么测试吗?
  14. JVM-常见JVM参数、如何查看JVM参数、如何动态设置JVM参数
  15. kali linux安装教程从官网开始。
  16. C# MVC 向页面传值方式
  17. 数字孪生开发平台 数字孪生开发成本 数字孪生开发平台cortona3d
  18. Tk 的基本概念-组件—Tkinter 教程系列03
  19. 练习假摔(视频, 超搞笑)
  20. 如何在 Windows 恢复环境中使用 Bootrec.exe 工具解决和修复 Windows Vista 中的启动问题

热门文章

  1. NYOJ 117 求逆序数
  2. Ubuntu16.04怎样安装Python3.6
  3. window下安装nvm、node.js、npm的步骤
  4. memcache运行机制(转)
  5. IIS7 + Tomcat7 整合共用80端口
  6. 面向对象基础回顾(二)
  7. circshift 函数详解
  8. 关于Linux的缓存内存 Cache Memory详解
  9. 量子遗传算法原理与MATLAB仿真程序
  10. latex使用小记录