紧接着,上一篇博文Linux 0.11内核之旅(四) :main.c之硬件初始化
继续描述main函数后半段的move_to_user_mode,再贴一下main函数代码
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024); //将main_memory_start开始的RAMDISK×1024个字节=0
#endif
mem_init(main_memory_start,memory_end); //内存初始化
trap_init(); //idt中断表初始化
blk_dev_init();//块设备初始化
chr_dev_init();//字符设备初始化,为空
tty_init();//tty设备初始化
time_init();//时间初始化
sched_init();//调度程序初始化
buffer_init(buffer_memory_end);//缓冲区初始化,创建管理缓冲区的双向链表,此处缓冲区大小为3M
hd_init();//硬盘以及硬盘中断初始化
floppy_init();//软盘以及软盘中断初始化
sti();//打开中断
/*******************************分析从这里开始*************************************/
move_to_user_mode();//指令实现从内核模式切换到用户模式(任务0)
/*******************************分析从这里结束*************************************/
//以下代码在用户模式(任务0)中执行
if (!fork()) { /* we count on this going ok *///触发系统中断0x80,调用system_call中断函数
//fork返回值为0的进程(子进程)执行init();
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause(); //任务0进入pause
}
在sti();打开中断之后,调度程序也因为,定时中断产生而被定时执行,基本上内核态的配置工作已经完成。
但操作系统是为了应用程序而服务的,没有应用程序,操作系统也没有存在的价值了。
所以在sti();之后的move_to_user_mode()正是完成从内核态切换到用户态的函数,也就是说在这个函数之后的代码都是工作在用户态下面的,可以理解为这之后的代码其实是一个应用程序,也是操作系统所接待的第一个应用程序task0。
让我们来看看move_to_user_mode的源码
切换到用户模式运行。
// 该函数利用iret 指令实现从内核模式切换到用户模式(初始任务0)。
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \
"pushl %%eax\n\t" \
"pushfl\n\t" \
"pushl $0x0f\n\t" \
"pushl $1f\n\t" \
"iret\n" \
"1:\tmovl $0x17,%%eax\n\t" \
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
我们可以看到这里是x86的汇编实现的宏,其实原理很简单,首先将堆栈段选择符SS压入堆栈,然后将EAX(里面放在之前的ESP的值)压入堆栈,然后是标志寄存器,压入堆栈,然后将0x0f压入堆栈(最后因为iretd的执行会弹出到CS段寄存器中),然后是标号l1的值,其实就是1:\tmovl $0x17,%%eax\n\t这条指令的段内地址,压入堆栈。
最后,最关键的一步,也是Linus常用的方法,那就是中断返回指令iretd,啥?中断返回指令,对啊,这里又不在中断里,为啥会用中断返回指令呢,其实用什么指令不重要,重要的是看看这个指令到底做了什么事情。
其实iretd指令要根据CPU的状态分很多情况讨论,它做了什么,但是大体的内容都是类似的,就是将刚刚压入堆栈的值,弹出到相应的段寄存器中。
这里,首先将标号1的值弹出到IP寄存器(指令指针寄存器),然后将0x0f弹出到CS寄存器,也即是00001111,低两位代表特权级3,也就是用户级,倒数第三位代表,从GDT还是LDT中取地址,这里是1,所以是从LDT中取,那么倒数第四位的意思是取LDT的第几个地址呢,这里是第1个地址的值(从0开始数),这个地址将最终作为段寄存器CS的寻址地址,这个值其实也就是,之前初始化函数sched_init里设定的init_task.task.ldt中的第1个地址,
void sched_init(void)
{
...
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); //将tss调用放在gdt的第四个位置
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); //将ldt调用放在gdt的第五个位置
...
ltr(0);//LTR指令是专门用于装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择子。LTR指令从GDT中取出相应的TSS段描述符,
lldt(0);//加载ldt到ldtr寄存器
...
}
init_task.task里的值是写死的,可以看到ldt表的第1项(从0开始数)的值为{0x9f,0xc0fa00},最终会被作为CS的段寻址值。
#define INIT_TASK \
{\
/* state etc */0,15,15, \
/* signals */0, {{0},}, 0,\
/* ec,brk... */0, 0, 0, 0, 0, 0,\
/* pid etc.. */ 0, -1, 0, 0, 0, \
/* uid etc */ 0, 0, 0, 0, 0, 0, \
/* alarm */ 0, 0, 0, 0, 0, 0, \
/* math */ 0, \
/* fs info */ -1, 0022, NULL, NULL, NULL, 0, \
/* filp */ {NULL,}, \
/* ldt[3]*/ {{0, 0}, \
{0x9f, 0xc0fa00}, /* 代码长640K,基址0x0,G=1,D=1,DPL=3,P=1 TYPE=0x0a*/ \
{ 0x9f, 0xc0f200},}, /* 数据长640K,基址0x0,G=1,D=1,DPL=3,P=1 TYPE=0x02*/ \
/*tss*/ {0, PAGE_SIZE + (long) (&init_task), 0x10, 0, 0, 0, 0, (long) &pg_dir,\
0, 0, 0, 0, 0, 0, 0, 0, \
0, 0, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, _LDT (0), 0x80000000, {0} },\
}
...
static union task_union init_task = {INIT_TASK,};
紧接着,标志寄存器的值出栈,依旧是弹出到标志寄存器中。
然后,ESP的值出栈,依旧是弹出到ESP寄存器中。
最后,0x17出栈,弹出到SS寄存器中。0x17,二进制为00010111,意思和刚刚的CS寄存器的含义很像,倒数两位,代表特权级,这里是3,也就是用户级,倒数第三位,从GDT还是LDT中取地址,这里是1,所以是从LDT中取,倒数四五位,取LDT的第2个地址(从0开始数),根据上面的东西,得到ldt表中的第二项为{ 0x9f, 0xc0f200},为SS的最终寻址。
所以这里总结一下move_to_user_mode,执行完之后,寄存器的变化
CS=task0的CS段
IP=1标签处
SS=task0的SS段
SP=SP
EFLAGS=EFLAGS
IP指定CPU跳转到"1:\tmovl $0x17,%%eax\n\t" \接着执行,而此时的SS堆栈段和CS程序段已经切换到task0指定的堆栈和程序段,并且最关键的特权级从原本的0(内核级)降权到3(用户级),表明此时系统已工作在用户态。
最后再将其他段寄存器赋值为:AX=DS=GS=FS=GS=0x17,意义同上,,实际寻址为{ 0x9f, 0xc0f200}。
至此,move_to_user_mode分析完毕,后面一篇博文将紧接着分析task0应用程序做的第一件事fork调用。