您的位置:首页 > 编程语言 > C语言/C++

实模式汇编代码header.S——准备实模式下C语言环境

2010-12-31 21:06 309 查看

3.2.3 准备实模式下C语言环境

好了,执行到setup代码了,header.S几乎所有的代码都在准备实模式下的C语言环境。在讲解这部分代码之前先回顾一下C语言程序关于程序中.text,.data,.bss等段的说明。由于历史原因,C程序一直由下列几部分组成:

1、 正文段。这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是经常执行的程序(如文本编辑程序、C编译程序、shell等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外事故而修改其自身的指令。

2、 初始化数据段。通常将此段称为数据段,它包含了程序中需赋初值的变量。例如, C程序中任何函数之外的说明:
int maxcount = 99; 使此变量以初值存放在初始化数据段中。

3、 非初始化数据段。通常将此段称为bss 段,在程序开始执行之前,内核将此段初始化为0。函数外的说明:
long sum[1000] ; 使此变量存放在非初始化数据段中。

4、 栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。然后,新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈, C函数可以递归调用。

5、 堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段顶和栈底之间。

现在来看看vmlinuz第二个512字节处,也就是_start:

105 # offset 512, entry point
106
107 .globl _start
108 _start:
109 # Explicitly enter this as bytes, or the assembler
110 # tries to generate a 3-byte jump here, which causes
111 # everything else to push off to the wrong offset.
112 .byte 0xeb # 0200处的第一个字节是跳转指令
113 #(其中,0xeb是该指令代码)。
114 .byte start_of_setup-1f # 0201是跳转距离,(start_of_setup-1f)其实就是setup中的头部长度。

第一条指令,这是一条短跳转,跳过了一些数据(从标号1到start_of_setup),由于以后会再提及这些数据,暂时不管他。注意这里,从_start到start_of_setup的代码是些伪指令,即setup的初始化头变量hdr,涉及的是内存布局,并不涉及具体的指令执行。此时,cs:eip指向的是start_of_setup处的代码,所以现在接着看start_of_setup:

240 .section ".entrytext", "ax"
241start_of_setup:
242#ifdef SAFE_RESET_DISK_CONTROLLER
243# Reset the disk controller.
244 movw $0x0000, %ax # Reset disk controller
245 movb $0x80, %dl # All disks
246 int $0x13
247#endif

上面代码用13号中断重设系统盘的磁盘控制器 ax=0x0 ,dl=0x80,不明白的请查看博文“BIOS系统服务 —— 直接磁盘服务

249# Force %es = %ds
250 movw %ds, %ax
251 movw %ax, %es
252 cld

先强制让附加数据段es的内容等于数据段ds的内容。接下来要为即将开启的C程序建立堆栈段了(其实C程序的运行条件很简单,只要你给我提供一个堆、一个栈,没堆栈不行!)。

259 movw %ss, %dx
260 cmpw %ax, %dx # %ds == %ss?
261 movw %sp, %dx
262 je 2f # -> assume %sp is reasonably set

上面的代码比较ds 和ss是否相等,注意ax里的还是ds的值,现在就是比较 ds和ss的值,再把dx设置为栈顶指针sp值。随后判断,如果ds=ss,则跳转到前面(f表前,b表后)的标号2处,注意, C语言中把数据段当做堆栈段使用;不相等就新建一个栈(一般情况下是不会相等的,因为肯定是会有数据的):

264 # Invalid %ss, make up a new stack
265 movw $_end, %dx
266 testb $CAN_USE_HEAP, loadflags
267 jz 1f
268 movw heap_end_ptr, %dx
269 1: addw $STACK_SIZE, %dx
270 jnc 2f
271 xorw %dx, %dx # Prevent wraparound

上面首先把栈底设置_ end给dx。还记得***bzImage时,链接setup.elf的那个脚本setup.ld吗?当时说它重要,这里就体现出来了,这个_end就来自那里,是整个setup.bin的结尾。

所以,此时此刻这个_end就应该作为setup阶段的C语言使用堆栈的临时栈顶。千万要注意,我们在链接vmlinux的时候使用的那个链接脚本也有一个_end标号,但是它随着vmlinux被压缩进了vmlinuz,所以不存在与setup.bin中的_end相冲突的情况。

不过C语言有可能还要使用堆,所以随后一个testb命令,让两操作数(byte)做与运算,只修改标志位,看内核头参数中有没有设置 CAN_USE_HEAP位。如果设置了CAN_USE_HEAP位,表示C语言可以使用堆,如果与运算结果等于0,即没有设置这一位则跳到上面标号1处。当然,我们看到前面第138行CAN_USE_HEAP的值为0x80,而在arch/x86/include/asm/Bootparam.h的46行定义的:
#define CAN_USE_HEAP (1<<7)

所以这一位是肯定有的,所以会用堆的顶部,即heap_end_ptr变量加到栈顶,随后将其值存放在dx寄存器中。heap_end_ptr来自166行:
heap_end_ptr: .word _end+STACK_SIZE-512

我们看到heap_end_ptr的值就是_end的地址,因为STACK_SIZE在arch/x86/boot/Boot.h中定义的值是512:
/arch/x86/boot/Boot.h中:
#define STACK_SIZE 512

顺便说一句,如何使用堆?其实就是通过c语言的malloc和alloc等函数人工分配数据空间。

继续走,到标号1,栈顶还要加上一个STACK_SIZE。为什么还要加一个STACK_SIZE呢?我们知道,x86系列处理器的栈是向下递增的,也就是说,压栈动作,ss:esp向低地址移动;出栈动作,ss:esp向高地址移动。所以我结合前面回忆的C语言程序的特点,再根据刚才的内核映像vmlinuz内存布局简单地对实模式下的部分画了一个图:

|--------------------|
| 命令行参数和 |
| 环境变量 |
|--------------------|
| 栈 |
| ↓ | -------------------------------------àesp
| STACK_SIZE |
| |
|--------------------|
| |
| ↑ |
|--------------------|
| 堆 |
|--------------------|+-- -------------------------------------à_end / heap_end_ptr
| | |
| 未初始化的数据 | ++ 由exec赋初值0
| | |
|--------hdr---------|+--
| 初始化的数据 | | -----------------------------------àds
|--------------------| ++ exec从程序文件中读取
| 正文 | |
|--------------------|+-- -----------------------------------àcs

从图中可以看到STACK_SIZE就是栈的大小,对于实模式下c语言环境,512字节的栈大小足够了。同时,由于_end和heap_end_ptr相等,所以,堆的起始地址就是_end。

好了,我们接着往下看:

273 2: # Now %dx should point to the end of our stack space
274 andw $~3, %dx # dword align (might as well...)
275 jnz 3f
276 movw $0xfffc, %dx # Make sure we're not zero
277 3: movw %ax, %ss
278 movzwl %dx, %esp # Clear upper half of %esp
279 sti

如果无进位,就转移到标号2处。标号2代码的作用是将dx中的栈顶地址按双字对齐,即将最低两位清零。到标号3处,这时候,ax是数据段寄存器ds的值,把它赋给堆栈段ss寄存器;dx是经过刚才一系列过程以后的栈顶地址,赋给esp寄存器。此时此刻,ss:esp开始工作!说明经过编译器编译的那些C代码可以工作了(主要就是那些C语言的函数使用堆栈段)。

从这里我们也可以看出,Linux中,内核数据段与堆栈段其实都来自于内核数据段基址,只是偏移不同,esp跨过了一部分内核数据段和堆,并向下、向低地址发展(如果有的话,即DS≠SS)。我们继续往下看:

281# We will have entered with %cs = %ds+0x20, normalize %cs so
282# it is on par with the other segments.
283 pushw %ds
284 pushw $6f
285 lretw

根据上面说的,grub是以ljmp指令过来的,跳过来的时候数据段被设置为X,即0x90000。所以跳过来这后,cs寄存器的值为0x90020。这里做一个修正,因为堆栈已经准备好,那么上面三条语句利用堆栈指令将$6f压栈,这样随后执行返回指令的话,就可以修正前面的偏差了,并且还可以测试一下刚刚建立的堆栈是否可靠,真是一举双得啊。

286 6:
287
288# Check signature at end of setup
289 cmpl $0x5a5aaa55, setup_sig
290 jne setup_bad

上面这段指令设置堆栈,对于正确的setup ,cmpl指令是总是对的,如果不对,就跳到setup_bad。

292# Zero the bss
293 movw $__bss_start, %di
294 movw $_end+3, %cx
295 xorl %eax, %eax
296 subw %di, %cx
297 shrw $2, %cx
298 rep; stosl

上面这段代码清空setup的bss段。注意和数据段的区别,BSS存放的是未初始化的全局变量和静态变量,数据段存放的是初始化后的全局变量和静态变量。

300# Jump to C code (should not return)
301 calll main

好啦,至此,代码奔向到C语言了,这main就是在boot/main.c中的main函数。后面的代码是一些错误处理,我们就不详细分析了。这里总结一下,汇编代码header.S从开始到 # offset 512, entry point功能和以前的bootsect一样。后面的功能和setup.S的一部分类似,主要的工作是设置setup header参数部分;设置堆栈;检查setup中的标签;清除BSS段;调用C入口main。此时cs:eip指向的是main,ss:esp指向的是堆栈栈顶,ds:edi指向哪儿不知道,也不重要。

303# Setup corrupt somehow...
304setup_bad:
305 movl $setup_corrupt, %eax
306 calll puts
307 # Fall through...
308
309 .globl die
310 .type die, @function
311die:
312 hlt
313 jmp die
314
315 .size die, .-die
316
317 .section ".initdata", "a"
318setup_corrupt:
319 .byte 7
320 .string "No setup signature found.../n"
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: