Linux 0.11内核之旅(五) :main.c之move_to_user_mode

紧接着,上一篇博文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调用。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值