Linux 内核源码解析---心得记录

系统启动全流程图

Linux 内核源码解析---心得记录_第1张图片

linux源码解读系列

关于内核源码解读的一系列资料:链接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状态从核心态切换到用户态)。
Linux 内核源码解析---心得记录_第2张图片
上面的用户态和核心态的区分由硬件设计实现,计算机对内存的使用都是按段计算的,而这个硬件设计,靠CPU的段寄存器(CS, DS)实现。又涉及到的CPL(Current Privilege Level),DPL(Destination Privilege level):
DPL:是用来描述目标内存段的特权级别,也就是要跳往的访问的
CPL:是用来描述当前的特权级,取决于执行的是什么指令 ,执行指令都要有PC(CS:IP)
---------CS表示一段内存区 ,CS中的一部分表示当前程序所处于的特权级
进入内核条件:目标特权级的数字要大于或等于当前特权级的数字(DPL>=CPL)
Linux 内核源码解析---心得记录_第3张图片
小结:
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 条件不成立直接挡住不让进入内核,否则进入内核。

Linux 内核源码解析---心得记录_第4张图片
通过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进行系统调用,例如:
Linux 内核源码解析---心得记录_第5张图片

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% = 0xef000%变量为:"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(160x0008是段选择符,16位会被放在edx中的过程偏移的低16位代替)4%变量为:"a" (0x00080000)

Linux 内核源码解析---心得记录_第6张图片

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函数,如下:
Linux 内核源码解析---心得记录_第7张图片
到此,就要开始真正去调用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

你可能感兴趣的:(linux,linux)