深入理解操作系统(二)

一.操作系统接口


1.什么是接口?

    Interface:electrical circuit linking one device with another and enabling data coded in one format to be transmitter in another(出自牛津辞典)

    从操作系统的角度来说,就是上层应用通过接口,进入系统,从而获得硬件支持。这其中内部的过程普通用户无需了解。更细致地来说就是普通的C代码再加上重要的函数构成了应用程序,而这些重要的函数构成了操作系统的接口,由于又是被系统提供,所以又称系统调用。


2.用户使用计算机的方式

    1).命令行

         命令其实就是一段用C语言写的程序,比如下面是用C语言写的一个简单的程序

#include <stdio.h>
int main(int argc, char * argv[])
{
    printf("ECHO:%s\n",argv[1]);
} 

经过编译之后:gcc -o output output.c

可以在命令行键入该命令:./output "hello"

就会得到下面的输出

其中output作为一个可执行文件,其中的"hello"作为一个shell,shell也是一个程序:/bin/sh

// shell程序的主体代码
int main(int argc, char * argv[])
{
    char cmd[20];
    while(1)        // 死循环
    {
        scanf("%s",cmd);
        if(!fork())
        {
            exec(cmd);
        }
        else
        {
            wait();
        }
}     

    2).图形按钮

           图形按钮基于一套消息机制:通过对硬件消息输入消息的获取,经过消息循环机制,最终通过绘图功能表现。

具体流程以鼠标点击为例:当鼠标点击后,通过中断将消息放置系统内部的消息队列,应用程序需要有一个消息循环模块,不停地将内核里的消息提取出来,每提取出一个消息,执行对应的函数及功能。


    3).应用程序

系统调用在POSIX中已经规定(IEEE制定的一个标准族),具体可以参考该项约定。



3.系统调用的实现


    1).内核态和用户态:简单来说就是CPU划分出的两个特权级,由处理器的"硬件设计"支持,其中内核态可以访问任何数据,用户态不能访问内核数据,这里,用CS的最低两位来表示:0是内核态,3是用户态(数字越大,表示特权越低)



    内核段和用户段:对应的内存区域,通过段寄存器:


DPL:描述目标段的特权级

CPL:当前特权级

用一个例子来具体的说明:


一个简单的直观想法就是,处于上层的应用不能随意地访问内核数据和代码段,否则会带来很大的安全隐患,比如存储在内核中密码等数据的泄露。

蓝色部分是特权级为3的代码段,相应的红色部分是特权级为0的内核段,假设蓝色部分可以直接访问红色部分,因为当前的 CPL = 3,DPL = 0,不满足DPL >= CPL 因此该项假设并不成立。由此我们可以得出一个结论就是,处于用户态的代码段只能直接访问同为用户态的代码段,不能直接访问处于内核态的代码段和数据。


    2).硬件提供"主动进入内核的方法"

    对于intel x86,利用中断指令int(这里我们一般指的是 int 0x80)进入内核,这也是用户程序发起的调用内核代码的唯一方式。对于一个完整的系统调用,核心流程分为以下三个部分:

  •     用户程序中包含一段又int指令的代码
  •     操作系统写中断处理,获取像调程序的编号
  •     操作系统根据编号执行相应代码

以一段简单的代码实现为例:



以上述为例:首先,由C语言库函数将格式化输出采用C语言处理方式转化成如下代码段

// linux/lib/write.c
#include <unistd.h>
_syscall3(int, write, int, fd, const char *buf, size_t, count)

之后需要将上述代码转化成带有 int 0x80 中断的代码段

// linux/include/unistd.h
// 此处代码对应带有三个参数的调用
#define _syscall3(type, name, atype, a, btype, b, ctype, c)\
type name(atype a, btype b, ctype c) \    // 此例中该处对应 int write(int fd,const char *buff, size_t count)
{
    long __res;\
// 下述代码将系统调用号置给%eax中,各参数分别置给相应寄存器,通过调用int 0x80 得到返回值存放在%eax,最终返回给%res
    __asm__volatile("int 0x80":"=a"(__res):""(__NR_##name),    // %eax存放返回值,置给%res 同时将__NR_write置给%eax
"b"((long)(a)),"c"((long)(b)),"d"((long)(c))));     // 将fd置给%ebx,buff置给%ecx,count置给%edx

if(__res >= 0) return
(type)__res;error =-__res; 
return -1;
}

// 这里__NR_write是系统调用号,放在eax中
#define __NR_write 4

最终这个中断代码再通过系统调用,开始进入到内核中


3).int 0x80中断处理

现在我们大致能了解到,用户程序要进入内核,关键点就在于 int 0x80中断处理


上层命令想进入内核,就需要依赖int 0x80中断,这就好比进入操作系统内部的桥梁,而系统调用号指明了进入内核后执行的步骤,待一切进行完成后,最后返回。所以这就需要进一步了解0x80中断

void sched_init(void)
{
    set_system_gate(0x80, &system_call);
} 
// linux/include/asm/system.h
#define set_system_gate(n, addr) \    // n:中断处理号(0x80), addr:中断处理地址(&system_call)
_set_gate(&idt[n],15,3,addr);        // 寻找80对应的表项,dpl = 3

// 下述代码主要初始化idt表
#define _set_gate(gate_addr, type, dpl, addr)\
__asm__("movw %%dx,%%ax\n\t" "movw %0,%%dx\n\t"\
"movl %%eax,%1\n\t" "movl %%edx,%2":\
:"i"((short)(0x8000+(dpl<<13)+type<<8))),"o"(*(( \
char*)(gate_addr))),"o"(*(4+(char*)(gate_addr))),\
"d"((char*)(addr),"a"(0x00080000))

由上述代码,0x80中断的本质作用其实是根据idt表查询找到中断需要转去的地址,然后跳转到该地址处执行,下图是idt表的格式:



其中将addr组装再处理函数入口点偏移,将DPL置为3,将0x00080000中的高四位组装到段选择符,因此现在新的PC被设为,cs=8,ip = addr,值得一提的是,GDT表DPL段为3,允许处于用户态的代码进行访问

此时因为cs的末两位为0,所以CPL = 0,因此可以访问内核代码和数据。


4).中断处理程序:system_call

成功进入内核后,通过中断处理程序,执行相应的功能

// linux/kernel/system_call.s
nr_system_calls=72 
.globl _system_call 
_system_call: cmpl $nr_system_calls-1,%eax
 ja bad_sys_call
 push %ds 
 push %es  push %fs pushl %edx  pushl %ecx  pushl %ebx  // 调用的参数
 movl $0x10,%edx  mov %dx,%ds  mov %dx,%es // 内核数据
 movl $0x17,%edx  mov %dx,%fs  // fs可以找到用户数据
 call _sys_call_table(,%eax,4) // a(,%eax,4)=a+4*eax,跳至该处地址的表执行
 pushl %eax //返回值压栈,留着ret_from_sys_call时用 ... // 其他代码 
ret_from_sys_call: popl %eax, 其他pop, iret

// _sys_call_table + 4*%eax就是相应系统调用处理函数入口,每个系统调用占4各字节(即函数指针)
// 对应函数表如下代码(存放在include/linux/sys.h)
fn_ptr sys_call_table[] = 
{ sys_setup, sys_exit,sys_fork, sys_read, sys_write,...};    // 偏移为4(从0开始)
// include/linux/sched.h
typedef int (fn_ptr*)();    // fn_ptr为函数指针

5).调用sys_write代码

此处涉及到操作系统的文件管理,暂时略去


6).总结



用户调用的printf首先通过C库函数转化成带有int 0x80段的代码,int 0x80段代码在系统初始化的时候做成了一个system_call。在用户调用printf时CPL=3,而int 0x80的DPL = 3。一旦进入内核态,CPL=0。再调用system_call中断处理,该中断处理又会根据传递进来的__NR_write=4去查sys_call_table表,最终调用sys_write。

猜你喜欢

转载自blog.csdn.net/adorkable_thief/article/details/80599188