内核文件kernel.bin是elf格式的二进制可执行文件,初始化内核就是根据elf规范将内核文件中的段(segment)展开到(复制到)内存中的相应位置。在分页模式下,程序是靠虚拟地址来运行的,无论是内核还是用户程序,它们对cpu来说都是指令或数据、没什么区别,交给cpu的指令或数据的地址一律被认为是虚拟地址。坦白说,内核文件中的地址是在编译阶段确定的,里面都是虚拟地址,程序也是靠这些虚拟地址来运行。但这些虚拟地址实际上是我们在初始化内核阶段规划好的,即想安排内核在哪片虚拟内存中,就将内核地址编译成对应的虚拟地址。而目前我们初始化的是内核,它在物理低端1MB内存中,初始化工作取决于这1MB物理内存中哪块空间可用,所以,现在还要看前面的内存分布图从中找块合适的内存空间来容纳内核映像。

其实大家早已经知道内核的入口虚拟地址是0xc0001500啦。但现在大家要假装不知道^_^,配合一下啊,咱们说一下0xc0001500是怎么来的。

物理内存中0x900处是loader.bin加载的地址,在loader.bin的开始部分是GDT,它可是必须要保留下来的,可不能覆盖,我们不打算在内核中重新定义它,以后都要指望它了。正如伟大领袖虽然仙逝了,但威望犹在,虽然loader的工作结束啦,但loader所完成的工作成果咱们还得继续发扬继续用。预计loader.bin的大小不会超过2000字节。所以咱们可选的起始物理地址是0x900+2000=0x10d0(不要把注意力放在这个奇怪的数上,偶然得出的)。内存很大,但也尽量往低了选,于是凑了个整数,选了0x1500做为内核映像的入口地址。

根据咱们的页表,低端1MB的虚拟内存与物理内存是一一对应的,所以物理地址是0x1500对应的虚拟地址是0xc0001500。这就解释了在5.3.1节中,链接命令ld中用-Ttext指定了代码段的起始虚拟地址,再把命令搬过来给大家看下:

ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin

好,现在咱们得说一下初始化内核的代码,见代码:

193 ;---------- 将kernel.bin中的segment拷贝到编译的地址 -----------
194 kernel_init:
195 xor eax, eax
196 xor ebx, ebx ;ebx记录程序头表地址
197 xor ecx, ecx ;cx记录程序头表中的program header数量
198 xor edx, edx ;dx 记录program header尺寸,即e_phentsize
199
200 mov dx, [KERNEL_BIN_BASE_ADDR + 42]
; 偏移文件42字节处的属性是e_phentsize,表示program header大小
201 mov ebx, [KERNEL_BIN_BASE_ADDR + 28]
; 偏移文件开始部分28字节的地方是e_phoff,
;表示第1 个program header在文件中的偏移量
202 ; 其实该值是0x34,不过还是谨慎一点,这里来读取实际值
203 add ebx, KERNEL_BIN_BASE_ADDR
204 mov cx, [KERNEL_BIN_BASE_ADDR + 44]
; 偏移文件开始部分44字节的地方是e_phnum,表示有几个program header
205 .each_segment:
206 cmp byte [ebx + 0], PT_NULL ; 若p_type等于 PT_NULL,说明此program header未使用。
207 je .PTNULL
208
209 ;为函数memcpy压入参数,参数是从右往左依然压入.;函数原型类似于 memcpy(dst,src,size)
210 push dword [ebx + 16] ; program header中偏移16字节的地方是p_filesz,
;压入函数memcpy的第三个参数:size
211 mov eax, [ebx + 4] ; 距程序头偏移量为4字节的位置是p_offset
212 add eax, KERNEL_BIN_BASE_ADDR
; 加上kernel.bin被加载到的物理地址,eax为该段的物理地址
213 push eax ; 压入函数memcpy的第二个参数:源地址
214 push dword [ebx + 8] ; 压入函数memcpy的第一个参数:目的地址;偏移程序头8字节的位置是p_vaddr,这就是目的地址
215 call mem_cpy ; 调用mem_cpy完成段复制
216 add esp,12 ; 清理栈中压入的三个参数
217 .PTNULL:
218 add ebx, edx ; edx为program header大小,即e_phentsize,;在此ebx指向下一个program header
219 loop .each_segment
220 ret
221
222 ;---------- 逐字节拷贝 mem_cpy(dst,src,size) ------------
223 ;输入:栈中三个参数(dst,src,size)
224 ;输出:无
225 ;---------------------------------------------------------
226 mem_cpy:
227 cld
228 push ebp
229 mov ebp, esp
230 push ecx ; rep指令用到了ecx,; 但ecx对于外层段的循环还有用,故先入栈备份
231 mov edi, [ebp + 8] ; dst
232 mov esi, [ebp + 12] ; src
233 mov ecx, [ebp + 16] ; size
234 rep movsb ; 逐字节拷贝
235
236 ;恢复环境
237 pop ecx
238 pop ebp
239 ret

对于可执行程序,我们只对其中的段(segment)感兴趣,它们才是程序运行的实质指令和数据的所在地,所以我们要找出程序中所有的段。

函数kernel_init的作用是将kernel.bin中的段(segment)拷贝到各段自己被编译的虚拟地址处,将这些段单独提取到内存中,这就是平时所说的内存中的程序映像。kernel_init的原理是分析程序中的每个段(segment),如果段类型不是PT_NULL(空程序类型),就将该段拷贝到编译的地址中。

现在内核已经被加载到KERNEL_BIN_BASE_ADDR地址处,该处是文件头elf_header。在我们的程序中,遍历段的方式是指向第一个程序头后,每次增加一个段头的大小,即e_phentsize。该属性位于偏移程序开头42字节处。为了以后遍历段时方便,避免了频繁的访问内存,在第200行,我们用寄存器dx来存储段头大小,这样,每遍历一个段头时,就直接从dx中获取段头大小,这将在第218行体现。

为了找到程序中所有的段,必须要获取程序头表。在文件开头偏移28字节处是属性e_phoff,该属性表示程序头表在文件中的偏移量,程序头表是程序头program header的数组,所以e_phoff也就是第1 个program header在文件中的偏移量。第201行,在内存e_phoff处取值,将得到的程序头表偏移量存入寄存器ebx。

我们需要的是程序头表的物理地址,由于此时的ebx还是程序头表文件内的偏移量,所以要将其加上内核的加载地址,这样才是程序头表的物理地址。所以在第203行为ebx加上了内核文件的加载地址KERNEL_BIN_BASE_ADDR。最终ebx寄存器做为程序头表的基址,用它来遍历每一个段,此时ebx指向程序中的第1 个program header。

我们已经知道,段是由程序头(program header)来描述的,一个程序头代表一个段。在知道了第一个程序头的地址后,为了遍历所有的程序头,还需要知道程序中程序头的数量,也就是段的数量,这是由elf_header中的属性e_phnum决定,它在elf_header中偏移为44。我们通常用cx寄存器来做循环计数器,所以在第204行,汇编语句“mov cx, [KERNEL_BIN_BASE_ADDR + 44]”将段的数量赋值给寄存器cx。

现在程序头表地址在寄存器ebx中,而且又知道了程序头表中段的数量,所以现在可以遍历每一个段的信息啦,其工作在代码第205~220行中完成。

在第206行,程序先判断下段的类型是不是PT_NULL,PT_NULL是在boot/include/boot.inc中定义的宏,其值为0,该意义表示空段类型。(PT_NULL也可以在linux系统的/usr/include/elf.h中找到其定义:#define PT_NULL 0)

在207行,如果发现该段是空段类型的话,就跨过该段不处理,跳到.PTNULL处,也就是第217行。

指定下一个段是通过在程序头表地址处加上一个段的大小e_phentsize来实现的,e_phentsize的值咱们已经将其存储在dx寄存器啦,所以在第218行,直接将ebx,也就是当前program header地址,加上edx,ebx便指向了下一个段的program header。edx的高16位为0,所以这里用add ebx, edx没有问题。

第209~216行,程序中的段通过mem_cpy函数复制到段自身的虚拟地址处。在这里,我们涉及到了函数调用约定的知识,不过为了叙述的更清楚,在这里我不想简单地说,在下一章中我们专门拿出一节来说这事儿。在此我还是本着够用的原则,把用到的部分给您说明白。

我们在此实现的函数是mem_cpy,不是c标准库中的memcpy函数,将来我们会在内核中实现memcpy。memcpy原型是void *memcpy(void *dest, const void *src, size_t n),功能是将src指向的地址空间处的连续n个字节拷贝到dest指向的地址空间。我们的学习它的用法,在汇编语言中用mem_cpy函数实现了它,此函数的原型相当于mem_cpy(void* dst, void* src, int size)。所以我们也要提供三个参数才能使用它。这三个参数都在程序头program header中,所以它们都可以基于ebx再增加适当的偏移量来得到。program header结构,很容易理解210~214行的代码。

第215行是调用 mem_cpy,这涉及到为该函数传入参数的问题。在汇编语言中传递参数的方法太多了,原因是汇编语言太灵活了,不怎么受约束,咱们可以访问到的资源太多了。所以,主调函数可以把参数放在寄存器中,也可以放在栈中,而栈就是内存,所以只要大家高兴,也可以把参数直接放到某块内存中,类似共享内存的方式来传递参数。主调函数以上面任意一种方式传递参数,被调函数都可以轻松地拿到参数。

一步步编写操作系统 49 加载内核2相关推荐

  1. 一步步编写操作系统 48 加载内核1

    其实,我们等了这一刻好久好久,即使我不说,大家也有这样的认识,linux内核是用c 语言写的,咱们肯定也要用c语言.其实...说点伤感情的话,今后的工作只是大部分(99%)都要用c语言来写,还有一些要 ...

  2. 一步步编写操作系统 51 加载内核4

    咱们的内容都是连栽的,如果您没看过我之前的文章,本节您是看不懂的. 接上节. 介绍完内核初始化的函数kernel_init后,本节代码部分还差一点点没说啦,下面看代码: -略 179 ;在开启分页后, ...

  3. 一步步编写操作系统 50 加载内核3

    接上节,在这里,我们把参数放到了栈中保存,大家注意到了,参数入栈的顺序是先从最右边的开始,最后压入的参数最左边的,其实这是某种约定,要不,为什么不先把中间的参数src入栈呢.既然主调函数按照从右到左的 ...

  4. 一步步编写操作系统 24 编写内核加载器

    这一节的内容并不长,因为在进入保护模式之前,我们能做的不多,loader是要经过实模式到保护模式的过渡,并最终在保护模式下加载内核.本节只实现一个简单的loader,本loader只在实模式下工作,等 ...

  5. 一步步编写操作系统 45 用c语言编写内核2

    在linux下用于链接的程序是ld,链接有一个好处,可以指定最终生成的可执行文件的起始虚拟地址.它是用-Ttext参数来指定的,所以咱们可以执行以下命令完成链接: ld kernel/main.o - ...

  6. 操作系统真象还原实验记录之实验七:加载内核

    操作系统真象还原实验记录之实验七:加载内核 对应书P207 1.相关基础知识总结 1.1 elf格式 1.1.1 c程序如何转化成elf格式 写好main.c的源程序 //main.c int mai ...

  7. 一步步编写操作系统 46 用c语言编写内核3

    再把上节代码贴出来, 1 //int main(void) { 2 int _start(void) { 3 while(1); 4 return 0; 5 } 有没有同学想过,这里写一个_start ...

  8. 一步步编写操作系统 44 用c语言编写内核1

    先来个简单的,欢迎我们神秘嘉宾--main.c.这是我们第一个c语言代码. 1 int main(void) { 2 while(1); 3 return 0; 4 } 它没法再简单啦,简单的程序似乎 ...

  9. 一步步编写操作系统 71 直接操作显卡,编写自己的打印函数71-74

    一直以来,我们在往屏幕上输出文本时,要么利用bios中断,要么利用系统调用,这些都是依赖别人的方法.咱们还用过一个稍微有点独立的方法,就是直接写显存,但这貌似又没什么含量.如今我们要写一个打印函数了, ...

最新文章

  1. 卷积神经网络CNN---权值共享
  2. tomcat jsvc 调优及JMX监控
  3. Adversarial Validation 微软恶意代码比赛的一个kenel的解析
  4. 图片碎片化mask动画
  5. SAS 中计算总和或者计算总数的方法
  6. SQL注入——基于报错的注入(五)
  7. Ajax updatepanel用法
  8. Linux进阶之路————磁盘查询
  9. Kubernentes
  10. 实际开发的存储过程_实际生产中的 Android SDK开发总结| 完结
  11. 前端组件化Polymer入门教程(7)——Local DOM
  12. 夜曲歌词 拼音_《夜曲》的歌词 - 歌手:周杰伦 (Jay Chou)
  13. 华为HCIE-CloudComputing备考笔记-2021.10
  14. 【无需卸载,丝滑关闭奇安信天擎开机自启动(步骤超简单)】
  15. canvas 画图移动端出现锯齿毛边的解决方法
  16. windows基于TCP/IP的简单文件/图片传输
  17. 倍福PLC的ModbusRTU设置
  18. golang中json.Number妙用
  19. 卡西欧350计算机度分秒转换,卡西欧FX-4500PA计算器怎样将如:12.58244度转换成度分秒啊...
  20. 超级巡警,专杀各类病毒木马

热门文章

  1. java程序员经常使用的Intellij Idea插件
  2. python之os、sys和random模块
  3. C#带按钮的文本框TextBoxContainButton
  4. pytorch自定义模型执行过程
  5. [剑指offer]面试题第[58]题[Leetcode][JAVA][第151题][翻转单词][字符串常用函数总结]
  6. html和css可以用在ssh里面么,在网站中使用SSH
  7. java 接口数据类型_Java 数据类型(中): 抽象类与接口
  8. draw python_科学网—Draw figures with Python - 高琳琳的博文
  9. python中sn的意思_python获取内存SN编号等信息
  10. alonedb.php on line 58,SHOPEX出现\core\include_v5\AloneDB.php on line 58的解决办法