接上节,在这里,我们把参数放到了栈中保存,大家注意到了,参数入栈的顺序是先从最右边的开始,最后压入的参数最左边的,其实这是某种约定,要不,为什么不先把中间的参数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寄存器赋值,从而恢复了程序执行流.
【再续】