接上节,在这里,我们把参数放到了栈中保存,大家注意到了,参数入栈的顺序是先从最右边的开始,最后压入的参数最左边的,其实这是某种约定,要不,为什么不先把中间的参数src入栈呢。既然主调函数按照从右到左的顺序在栈中压入参数,被调函数中必须分清楚这三个参数分别在栈中哪个位置。栈是向下扩展的,这一点通过push指令压栈时,栈指针esp的值越来越小能体现出来,所以最后压入的第1个参数是离栈顶(esp指向的地址)最近,最先入栈的第3个参数离栈顶最远。我们来看下在参数入栈后并调用函数时,栈中布局是什么,还是拿call mem_cpy为例。如图

由于栈指针esp已经在loader.S中被加上了0xc0000000,所以其栈中地址都是内核所在的0xc0000000以上的高地址。用call指令进行函数调用时,cpu会自动在栈中压入返回地址,由图可见,当调用kernel_init函数时,当时的栈指针是0xc00008fc,所以kernel_init的返回地址被存储在0xc00008fc处。栈中地址0xc00008f8处的内容是提供给函数mem_cpy的第三个参数,即size。地址较低的0xc00008f4处是它的第二个参数,即src地址,0xc00008f0处是它的第一个参数,即dst。

在mem_cpy的实现中,我们访问栈中的参数是基于ebp来访问的,这通常意味着要将esp的值赋给ebp。由于不知道ebp中的值是不是重要,好的习惯是提前将ebp备份起来,这就是在第228行的目的,将ebp入栈备份,这样在函数结束时能够将其恢复。我们在第229行将esp赋值给了ebp。所以上图中,标出了ebp的指向,由于后来在第230行又将ecx入栈,故esp已经小于ebp。

栈中每个单元占用4字节,既然是基于ebp来获得栈中的参数,那么如图所示,第1个参数dst的地址是ebp+8,第2个参数src的地址是ebp+12,第3个参数size的地址是ebp+16。分别对这些地址用中括号取值后,便可以得到实际的参数。

在继续往下说之前,要给大家介绍个数据复制小团队。

首先要说一下字符串“搬运”指令族:movsb、movsw、movsd。其中的movs代表move string,后面的b代表byte,w代表word,d代表dword。所以movsb的功能是搬运(复制)1字节,movsw的功能是搬运(复制)2字节,movsd的功能是搬运(复制)4字节。数据从哪里来,搬到哪里去呢?这三条指令是将DS:[E]SI指向的地址处的1或2或4个字节搬到ES:[E]DI指向的地址处,16位环境下源地址指针用SI寄存器,目的地址指针用DI寄存器,32位环境下源地址则用ESI,目的地址则用EDI。话说虽然这三个指令叫字符串指令,但它们可不是只用在字符串上,因为字符串中的字符不也是按字节来存储吗,任何数据在内存中都以字节存储单元来访问,字符串只是表相,本质上是复制字节,所以它更多的被通用于复制数据。

以上三个命令只是复制固定的字节数,每执行一次就复制1字节或2字节或4字节,如果大量的数据需要复制,则需要连续的运行,所以要介绍另外一个指令rep。

rep指令是repeat重复的意思,该指令是按照ecx寄存器中指定的次数重复执行后面的指定的指令,每执行一次,ecx自减1,直到ecx等于0时为止,所以在用rep重复执行某个指令之前,一定要将ecx寄存器提前赋值。

似乎说完了,但其实还差点什么,您想,如果想要复制一大块数据的话,总该有人更新数据的来源和目的地吧。movs [bwd]只是从[e]si指向的地址处搬运1、2、4字节到[e]di指向的地址处,它不会自动更新[e]si和[e]di。咱们总不能翻来覆去从同一个源地址搬运数据到另一个相同的目的地址吧。所以,cld和sld指令就派上用场了,这两个指令本质上是控制重复执行字符串指令时的[e]si 和[e]di的递增方式,递增方式是指它们的值逐渐变大还是逐渐变小,也就是说,地址是往高地址方向变化,还是往低地址方向变化,这就是所说的方向。cld是指clean direction,该指令是将eflags寄存器中的方向标志位DF置为0,这样rep在循环执行后面的字符串指令时,[e]si和[e]di根据使用的字符串搬运指令,自动加上所搬运数据的字节大小,这是由cpu自动完成的,不用人工干预。比如,执行一次movsd,[e]si和[e]di就自动加4,执行一次movsb,[e]si和[e]di就自动加1。有清除方向标志位就会有设置方向标志位,std是set direction,该指令是将方向标志位DF置为1,每次rep循环执行后面字符串指令时,[e]si和[e]di自动减去所搬运数据的字节大小。

也许cpu认为地址由低向高处发展是理所应当的,这无须设置,所以此时DF标志为0。当由高地址向低地址发展时,这不是正常自然的现象,所以需要强调一下,故要将DF标志置为1。

注意,并不是在任何字符串控制指令中[e]si和[e]di都同时增减,这要看字符串操作指令是否都用到了它们,处理器只会增加用到的那个。字符串操作指令有很多,比如有movs[bwd]、ins[bwd]和outs[bwd]、lods[bwd]和stos[bwd],esi和edi并不是被以上三组指令同时使用,只有movs[bwd]才同时使用esi和edi,通过rep指令组合执行时,esi和edi根据DF位的值自增或自减。ins[bwd]是从端口读入数据到内存的目的地址,故只涉及到edi的自增自减。outs[bwd]是把内存中的源数据写入端口,故只涉及到esi的自增自减。lods[bwd]是把内存中的源数据加载到寄存器al、ax或eax,自增自减操作也只涉及到esi。而stos[bwd]是将al、ax、eax中的值写入到内存中的目的地址,故也只涉及到edi的自增自减。

好啦,在稍微扩展了一小下之后,咱们回到正题。

有了movs[bdw]指令族、重复执行指令rep,方向指令cld和std,这三剑客在一起配合工作就能够自由复制任何大块数据啦。万事俱备,回到正题。

第227行的cld指令其实放在movsb之前就行,它是用于清除方向标志,让数据的源地址和目的地址逐渐增大。

由于外层函数也要用ecx做为遍历段的循环计数,所以您明白了,这里的第230行为什么要将ecx入栈备份啦,这样在ecx用完之后,在mem_cpy执行结束前通过pop指令将ecx和ebp恢复,以便外层遍历段的循环中保持ecx正确。

在第231~233行,为复制工作所需要的条件初始化,esi和edi指向了要复制的段的来源地址和目的地址,ecx是为rep指令做准备的,指定了调用movsb指令的次数。在此提醒一下,段寄存器DS和ES在进入保护模式之初就被赋成相同的选择子了,它们都指向同一个段描述符,故它们在此工作正确,请大伙儿放心。

一切就绪之后,在第234行,rep movsb,这三剑客团队就开始合作啦。

mem_cpy返回后,程序流程回到第216行,这是清理在调用mem_cpy之前在栈中压入的size,src,dst,这三个参数共占3*4=12字节,所以将esp加上12,于是栈顶跨过了它们,这三个参数所占的空间可被其它压栈操作覆盖。

每个函数中都要有个返回指令,这里用的是ret指令,以后我们还会接触到其它返回指令。之前在用call指令调用函数时,无论是调用kernel_init还是mem_cpy,cpu都会将函数的返回地址压入栈中保存,这是为函数体中的ret指令准备的,换句话说函数不会自己返回,是通过ret来返回的。ret指令将栈顶中的值做为返回地址,所以,一定要确保在调用ret时,位于栈顶处的数据是正确的返回地址。一般情况下,我们在函数体中保证push操作和pop操作配套成对,正如在mem_cpy的实现中,有两个push入栈操作,在函数返回前就要有两个pop出栈操作。

咱们的函数中用的都是ret近返回指令,所以只会在栈顶弹出4字节的数据做为代码段的偏移地址为EIP寄存器赋值,从而恢复了程序执行流.

【再续】

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

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

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

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

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

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

    内核文件kernel.bin是elf格式的二进制可执行文件,初始化内核就是根据elf规范将内核文件中的段(segment)展开到(复制到)内存中的相应位置.在分页模式下,程序是靠虚拟地址来运行的,无论 ...

  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. 2021-02-25 matlab 字符串和数字同时写入excel
  2. Python 面向对象与 C++、Java 的异同
  3. BZOJ-2588-Count-on-a-tree-SPOJ10628-LCA+主席树
  4. mysql中case when then 的使用
  5. vue router返回上一页
  6. Leetcode之javascript解题(No33-34)
  7. 7.Oracle数据库SQL开发之 算术运算
  8. 【Mendeley】自定义文献引用格式(cite style)并保存
  9. 带通滤波器电路图大全(三款带通滤波器电路设计原理图详解)
  10. IE插件加载问题调试
  11. Navicat连接Oracle
  12. java.lang.NoClassDefFoundError: org/apache/hive/service/cli/thrift/TCLIService$Iface
  13. 映像劫持 Image Hijack
  14. python中、函数定义可以不包括以下一对圆括号_在python中,参数在函数定义的圆括号对内指定,用分号分割...
  15. 学phyton第一天
  16. 爱加密so VMP浅析
  17. 无线技术—安全认证技术
  18. T1005: 地球人口承载力估计(信息学一本通C++)
  19. 计算机自带输入法在哪里设置方法,Windows7设置默认输入法_Win7默认输入法怎么设置?-192路由网...
  20. Spring AOP

热门文章

  1. 一个容易被忽视的css选择器
  2. java集合类分析-hashset
  3. LoadRunner中Action的迭代次数的设置和运行场景中设置
  4. ajax请求web服务返回json格式
  5. CSS中class优先级问题
  6. 把东西从学校搬回来了
  7. 语言差异引起的问题解决一例
  8. [Leedcode][JAVA][第209题][长度最小的子数组][滑动窗口][前缀和][二分查找][双指针]
  9. HDU - 5438 Ponds 拓扑 dfs
  10. pythonxml模块高级用法_Python利用ElementTree模块处理XML的方法详解