0%

Int 0x80

什么是系统调用

上层应用是怎么使用操作系统的呢?操作系统提供系统调用来供上层应用来和操作系统交互,就相当于OS为上层应用暴露了一堆接口,应用使用这些接口来完成相应的功能。举个例子,当你使用printf("hello, world!");时,printf是怎么把 hello, world! 输出到控制台上的呢?这里就是printf通过系统调用接口write,将其输出到了控制台上。那为什么不是printf直接将内容输出,还要通过系统调用write来做呢?展示控制台的这个屏幕,称其为显存,说白了就是一块硬件,那么你在屏幕上显示hello, world!,其实就是在往显存这个硬件中写入数据,而往硬件中写入数据是进程控制CPU去做的,也就是内核部分做的事情,所以write这个动作就属于系统调用了,需要使用内核态去完成write这个动作。那么就知道了为什么printf不能直接在屏幕输出内容,在屏幕输出内容相当于在硬件写入数据,需要内核态去做这件事,而printf属于用户态,是没有办法直接访问内核数据的,所以需要借助提供出来的write系统调用接口来完成。

操作系统如何进行权限控制

上面提到了用户态和内核态,那么怎么区别正在执行的程序是属于内核态还是用户态呢,程序也就是一条条指令,也就是怎么区别当前的指令属于用户态还是内核态,就是通过该指令的地址判断的:CS:IP

CS和IP是8086CPU中两个关键的寄存器,它们指示了CPU当前要读取指令的地址。CS : 代码段寄存器;IP : 指令指针寄存器。在8086机中,任意时刻,CPU将CS:IP指向的内容当作指令来执行。

通过该指令的地址CS:IPCS的低两位表示当前处于内核态还是用户态,0表示内核态,1表示用户态。当当前指令的CPL <= 要访问的数据段DPL时,才能够进行访问。

CPL:当前内存段的特权级。DPL:目标内存段的特权级。

printf(“hello, world!”)源码解析

前面我们提到了,printf函数通过系统调用write函数,将内容输出到显存上。其中还涉及到了用户态到内核态的转变,但是用户态的内存 CPL = 3,内核态的内存段 DPL = 0,本来是不能访问的,这里是怎么做到用户态调用内核态代码的呢?我们一点点来分析。

这里我们以linux.011举例,printf函数最终会调用到内核态的sys_write(_syscall3宏展开)

1
2
3
4
5
int printf (const char *format, ...) {
...
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
...
}
include/unistd.h
1
2
3
4
5
6
7
8
9
10
11
12
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}

这里使用了一段内联汇编代码+宏,将宏展开后其实就是标准的write函数,这里不做代入,来看下这里最关键的int 0x80,也是本文要描述的重点对象。

int 0x80

int 0x80怎么解释呢,int是一个中断指令,0x80代表使用idt表中位与0x80位置的中断处理函数来进行中断处理。那么这里又牵扯到了idt表,他又是个什么东西呢。

idt表: 在发生中断时,会选择一个中断处理函数去执行,idt表就是一个存放了所有中断处理函数地址的数组,当执行int 0x80的时候,就是从idt表中取出0x80对应存放的中断处理函数地址,并执行该中断处理函数。除了idt表外,还有gdt、ldt,详细可查阅资料了解。

也就是说,在执行int 0x80的时候,就直接跳到了内核态,我们来看下int 0x80到底做了些什么。

idt[0x80] 初始化过程

kernel/sched.c
1
2
3
4
void sched_init(void) {
...
set_system_gate(0x80,&system_call);
}
include/asm/system.h
1
#define set_system_gate(n,addr) _set_gate(&idt[n],15,3,addr)
include/asm/system.h
1
2
3
4
5
6
7
8
9
10
#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))
idt数据结构
1
2
3
4
5
typedef struct desc_struct {
unsigned long a,b;
} desc_table[256];

extern desc_table idt,gdt;

sched_init调用了_set_gate(&idt[0x80], 15, 3, &system_call),这里的system_call就是系统调用,我们将这里的四个参数一一对应下

  • gate_addr -> idt[0x80],gate_addr就是idt表中下标为0x80的那个元素,根据idt的数据结构能看出,一个idt有64位。
  • type: 15,这个跟类型相关的不是关注重点,可以忽略。
  • dpl: 3,前面我们提到cpl为当前指令特权级,dpl为目标段特权级,可想而知,这里可能将目标段指令dpl置为了3
  • addr -> 指中断处理函数地址,即system_call的地址

根据参数,我们大概可以推测出来一些东西,我们要将中断处理函数的地址typedpl全部塞到大小为64bit的idt[0x80]中,接下来就是解释下这段汇编做了什么就能理解怎么塞的了。
我用伪代码的形式重新描述下

1
2
3
4
5
6
7
8
9
10
11
12
// init
char* edx = addr // edx寄存器保存system_call地址 -> "d" ((char *) (addr))
char* eax = 0x0008 0000 // eax寄存器置为0x0008 0000 -> "a" (0x00080000))

eax = edx & 0x0000 1111 // 将system_call地址的低16位赋值给eax的低16位 -> movw %%dx,%%ax

edx = edx & 0x1111 0000 & (0x8000+(dpl<<13)+(type<<8)) // 将dpl和type位移到对应的位置,赋值给edx的低16位

idt[0x80].a = eax // 将eax赋值到idt[0x80]的低32位 -> movl %%eax,%1 (*((char *) (gate_addr)))

idt[0x80].b = ebx // 将ebx赋值到idt[0x80]的高32位 -> movl %%edx,%2 (*(4+(char *) (gate_addr)))

赋值完成,接下来我们看下idt详细的数据结构。

1
2
3
4
5
6
7
8
9
10
struct gate_struct {
u16 offset_low;
u16 segment;
struct idt_bits bits;
u16 offset_middle;
#ifdef CONFIG_X86_64
u32 offset_high;
u32 reserved;
#endif
} __attribute__((packed));
1
2
3
4
5
6
7
struct idt_bits {
u16 ist : 3,
zero : 5,
type : 5,
dpl : 2,
p : 1;
} __attribute__((packed));

idt数据结构

那么我们再来想下,这里的系统调用是怎么实现用户态代码CPL = 3,调用到DPL = 0的内核中断处理函数的

  1. 用户态代码访问idt[0x80]
  2. 用户态代码CPL与idt[0x80]的DPL都为3,可以访问
  3. 使用idt[0x80]查询system_call的地址,此时会使用到段选择符
  4. 这里的段选择符为0x0008,段选择符就是用来访问system_call代码段地址的CS,展开为CS=0000000000001000,前面讲到CS的末两位即为CPL,也就是说中断处理函数的代码段地址CPL = 0
  5. 所以总结来看,就是idt[0x80]帮助我们完成了用户态代码调用内核态的过程。
-------- 本文结束 感谢阅读 --------