南宁定制网站建设,企业seo如何优化,自助建设分销商城网站,休闲旅游产品营销网站的建设策略内核文件kernel.bin是elf格式的二进制可执行文件#xff0c;初始化内核就是根据elf规范将内核文件中的段#xff08;segment#xff09;展开到#xff08;复制到#xff09;内存中的相应位置。在分页模式下#xff0c;程序是靠虚拟地址来运行的#xff0c;无论是内核还是…内核文件kernel.bin是elf格式的二进制可执行文件初始化内核就是根据elf规范将内核文件中的段segment展开到复制到内存中的相应位置。在分页模式下程序是靠虚拟地址来运行的无论是内核还是用户程序它们对cpu来说都是指令或数据、没什么区别交给cpu的指令或数据的地址一律被认为是虚拟地址。坦白说内核文件中的地址是在编译阶段确定的里面都是虚拟地址程序也是靠这些虚拟地址来运行。但这些虚拟地址实际上是我们在初始化内核阶段规划好的即想安排内核在哪片虚拟内存中就将内核地址编译成对应的虚拟地址。而目前我们初始化的是内核它在物理低端1MB内存中初始化工作取决于这1MB物理内存中哪块空间可用所以现在还要看前面的内存分布图从中找块合适的内存空间来容纳内核映像。
其实大家早已经知道内核的入口虚拟地址是0xc0001500啦。但现在大家要假装不知道^_^配合一下啊咱们说一下0xc0001500是怎么来的。
物理内存中0x900处是loader.bin加载的地址在loader.bin的开始部分是GDT它可是必须要保留下来的可不能覆盖我们不打算在内核中重新定义它以后都要指望它了。正如伟大领袖虽然仙逝了但威望犹在虽然loader的工作结束啦但loader所完成的工作成果咱们还得继续发扬继续用。预计loader.bin的大小不会超过2000字节。所以咱们可选的起始物理地址是0x90020000x10d0不要把注意力放在这个奇怪的数上偶然得出的。内存很大但也尽量往低了选于是凑了个整数选了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_NULLPT_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地址加上edxebx便指向了下一个段的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这涉及到为该函数传入参数的问题。在汇编语言中传递参数的方法太多了原因是汇编语言太灵活了不怎么受约束咱们可以访问到的资源太多了。所以主调函数可以把参数放在寄存器中也可以放在栈中而栈就是内存所以只要大家高兴也可以把参数直接放到某块内存中类似共享内存的方式来传递参数。主调函数以上面任意一种方式传递参数被调函数都可以轻松地拿到参数。