关于内核源码解读的一系列资料:链接1,链接2
从内核源码的init/main.c开始阅读。
fork()函数
涉及到linux 内核源码fork()函数的理解。
相关资料:https://blog.51cto.com/u_13064014/5079734
声明与实现:
/* init/main.c */
static inline _syscall0(int,fork)
/*
* 以汇编的形式调用Linux的系统调用软中断int $0x80,并在eax寄存器指定系统调用的功能号
* int $0x80 : int 表示产生软中断,$0x80 表示系统调用号
* "=a" 表示用寄存器 eax,当汇编语句执行后,把 eax 的值传给变量__res,作为返回值
* “0” 表示和第 0 个输出变量有相同的约束,即使用寄存器 eax,把__NR_name的值赋给 eax,指明系统调用功能号
* __NR_##name :##为连接号
*/
/* include/unistd.h */
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
内嵌汇编,分为四部分
__asm__("汇编语句"
:输出寄存器
:输入寄存器
:会被修改的寄存器
)
陷入内核
陷入指令是指用户程序所依靠的指令,用于发起系统调用,请求操作系统提供服务。陷入指令有其中一点特殊在于,其只能在用户态下执行,而不可以在核心态下执行。用户程序执行陷入指令,相当于把CPU的使用权主动交给了操作系统内核程序(CPU状态会从用户态切换到核心态),之后操作系统内核程序再对系统调用请求做出相应的处理。处理完成后,操作系统内核程序又会把CPU的使用权还给用户程序(即CPU状态从核心态切换到用户态)。
上面的用户态和核心态的区分由硬件设计实现,计算机对内存的使用都是按段计算的,而这个硬件设计,靠CPU的段寄存器(CS, DS)实现。又涉及到的CPL(Current Privilege Level),DPL(Destination Privilege level):
DPL:是用来描述目标内存段的特权级别,也就是要跳往的访问的
CPL:是用来描述当前的特权级,取决于执行的是什么指令 ,执行指令都要有PC(CS:IP)
---------CS表示一段内存区 ,CS中的一部分表示当前程序所处于的特权级
进入内核条件:目标特权级的数字要大于或等于当前特权级的数字(DPL>=CPL)
小结:
1、在系统初始化的时候(head.s执行的时候)会针对内核态的代码和数据建立GDT表向,然后对应的DPL就等于0(当时已经初始化好了0),DPL在GDT中
2、最后转到用户态进行执行,启动用户执行程序,CS里的CPL=3,这个CPL=3也是初始化时候设置的,(推到用户态时候设成3)之后就一直保持3
3、当跳到内核时候CPL=0,回到用户时候CPL就又等于3
4、每一次跳转移动(jmp,mov)都要访问GDT表
5、这时候检查DPL>=CPL?
6、由于DPL=0,CPL=3 条件不成立直接挡住不让进入内核,否则进入内核。
通过int $0x80 中断触发系统调用陷入内核
i386的系统结构,支持256个中断向量,软件层从0x20号中断向量开始使用,共224个,其中80号中断向量在系统中用于系统调用,所以才可以通过汇编"int $0x80",从用户态切换到内核态。
int $0x80
操作系统提供了指令 int $0x80 来主动进入内核:
1)用户程序或C库中通过eax寄存器设置系统调用号,然后包含 int $0x80 指令代码,(通过内联汇编插入)
2)内核源码写中断处理或执行内核源码已有的中断处理,获取想调程序的系统调用号,
3)操作系统根据系统调用号执行相应的代码
调用系统函数时会通过内联汇编代码插入int 0x80的中断指令(不仅会插入中断指令,还会将系统调用号设置给 %eax 寄存器,并在ebx,ecx,edx寄存器中传入参数)。
通常应用程序都是使用具有标准接口定义的C函数库间接的使用内核的系统调用,即应用程序调用C函数库中的函数,C函数库中再通过int 0x80进行系统调用,例如:
int $0x80 中断在内核源码中的处理
int $0x80 中断需要查询中断表idt,找到对应的中断门。而内核初始化的时候,设置了0x80号中断对应的中断门,在sched_init()函数最后一行代码设置0x80号对应的中断门描述符,使得 int $0x80 中断就是调用 system_call 这个函数进行处理。如下:
/* init/main.c */
sched_init(); //调度初始化函数
/* kernel/sched.c */
set_system_gate(0x80,&system_call); //&system_call:为int 0x80对应中断处理函数地址
/* kernel/system.h */
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr) //idt是中断向量表基址, 15表示此中断号对应的是陷阱门
...
#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))
/*include/linux/head.h*/
typedef struct desc_struct {
unsigned long a,b;
} desc_table[256];
extern desc_table idt,gdt;
嵌入式汇编注释:
1、mov: 为寄存器移动指令, 例如movw dx,ax; 即为将 dx 的值赋予 ax, 指令后面的“w”为长度的指定w=word=16位=2个字节;相应的“l”=long=32位=4字节。
2、% :AT&T汇编在引用寄存器时要在前面加1个%,%%是因为GCC在编译时会将%视为特殊字符,拥有特殊意义,%%仅仅是为了汇编的%不被GCC全部转译掉。
3、ax 与 eax :ax与eax之间是有联系的,他们并不是孤立的,eax为32位寄存器,ax为16位寄存器,而ax就是eax的低16位。dx与edx具有相同的关系。
4、%0、%1、%2:分别代表%0…%2变量。
5、第1个":“后面是输出寄存器,第2个”:“后面是输入寄存器,第3个”:"后面是会被修改的寄存器(这里没用到,省略了)。
":"后面语句的顺序,分别对应着0…4变量,解析如下:
#输入项是立即数,0x8000对应P=1;(dpl<<13)对应DPL=3;(type<<8)对应TYPE=15,D=1,类型码=111(陷阱门);最终0% = 0xef00。
0%变量为:"i"((short)(0x8000+(dpl<<13)+(type<<8)))
#放入对应的idt[]低4字节中。
1%变量为:"o" (*((char *) (gate_addr)))
#放入对应的idt[]高4字节中。
2%变量为:"o" (*(4+(char *) (gate_addr)))
#把偏移地址addr值放入edx中,一共32位(高16位,低16位)。
3%变量为:"d" ((char *) (addr))
#把0x00080000值放入eax中,一共32位(高16位0x0008是段选择符,低16位会被放在edx中的过程偏移的低16位代替)。
4%变量为:"a" (0x00080000)
6、在这段程序的变量前面都加了明确的限定,“i” 为将立即数; "o"为操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址; "d"为将变量放入edx; "a"为将变量放入eax。
7、汇编语句解析
# edx的低16位放到eax的低16位,eax原来为0x00080000,执行此语句后:eax = 0x00080000 | (0x0000ffff & addr)
movw %%dx,%%ax
# %0的低16位放入dx,原来%0为"i"((short)(0x8000+(dpl<<13)+(type<<8))) = 0xef00;执行此语句后:dx = 0xef00
movw %0,%%dx
# eax的值放入%1中,经过上面语句,现在eax = 0x00080000 | (0x0000ffff & addr);%1 = gate_addr = &idt[n]
# 执行此语句后%1 = 0x00080000 | (0x0000ffff & addr),表示将&idt[n]描述符的低4字节设置为%1
movl %%eax,%1
# edx的值放入%2中,经过上面语句,现在edx = (0xffff0000 & addr) | 0xef00;%2 = gate_addr + 4= &idt[n] + 4
# 执行此语句后%2 = (0xffff0000 & addr) | 0xef00,表示将&idt[n]描述符的高4字节设置为%2
movl %%edx,%2
至此以后,每当执行 int $0x80 中断指令时,都会调用system_call函数,然后再调用sys_call_table,并根据%eax跳转到相应的处理函数执行:
/*kernel/system_call.s*/
system_call:
cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在eax中置-1并退出
ja bad_sys_call
push %ds # 保存原段寄存器值
push %es
push %fs
...
call sys_call_table(,%eax,4) # 间接调用指定功能C函数
...
sys_call_table(,%eax,4) = sys_call_table + %eax*4:系统调用对应的函数表地址+4*系统调用号
%eax为系统调用号;4表示4字节,因为每个函数指针占4字节。
sys_call_table 是一个全局函数数组,对应了系统调用的所有函数列表(与系统调用号对应):
/*include/linux/sys.h*/
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };
/*include/linux/sched.h*/
typedef int (*fn_ptr)();
例如系统调用号为__NR_write 4,则调用sys_write函数,如下:
到此,就要开始真正去调用sys_write了。
typedef基本数据类型:
typedef int a;
a abc; //等价于int abc
typedef结构体:
typedef struct a {
int a;
int b;
} abc;
abc aaa; //等价于struct a aaa
typedef数组:
typedef int a[5];
a aa; //等价于int aa[5],这里aa的本质,是具有5个元素的int类型数组
也就是说,typedef int a[5];,使得a与int[5]等价,当然C语言没有这样的写法,希望能够理解,a就是代表具有5个int类型元素的数组。
typedef struct desc_struct{
unsigned long a, b;
}
desc_table[256];
desc_table idt, gdt;
这里idt就是struct desc_struct idt[256],gdt同理。
Linux内核笔记001 - Intel X86 CPU 系列的寻址方式
Linux内核笔记002 - i386 的页式内存管理机制
Linux内核笔记003 - Linux内核代码里面的C语言和汇编语言
Linux内核笔记004 - 从内存管理开始,认识Linux内核
Linux内核笔记005 - 越界访问内存,Linux内核处理过程
Linux内核笔记006 - 交换分区
Linux内核笔记007 - 内存管理的进一步封装
Linux内核笔记008 - 中断的概念及硬件支持
Linux内核笔记009 - 中断、异常、陷阱、Bottom half、信号
原作者:https://zhuanlan.kanxue.com/user-815036.htm
https://sourceware.org/binutils/docs/as/
伪指令:以“.”开头的都是仅仅给汇编器看的汇编指令,用于指导汇编器如何汇编,也称伪指令。
.word
语法: .word expressions
预留 2 个字节,并将该 2 个字节的内容赋值为 expression,如果是用逗号隔开的多个expression,则为预留多个这样的 2 字节,并将它们的内容依次赋值。例如:
num: .word 0x100
更多可参考:https://www.cnblogs.com/xiaojianliu/articles/8733507.html