Linux0.11 系统调用进程创建与执行(九)(上):https://developer.aliyun.com/article/1597307
三、调用 fork 创建进程 1(init)
fork 函数是个系统调用,此处由进程 0 在用户态调用 fork 函数来创建进程 1。fork 函数触发的中断,由 kernel/system_call.s 中 system_call 函数响应。fork 函数定义如下(可参考 三、fork 函数定义 ):
static inline int fork(void) { long __res; __asm__ volatile ("int $0x80" // 调用系统中断 0x80 : "=a" (__res) // 返回值 => eax(__res) : "0" (2)); if (__res >= 0) return (type) __res; errno = -__res; return -1; }
1、中断描述符
中断描述符表位于 0x000054b8 处,系统调用中断号为 0x80(128),idt[128] 其值为:EF00:87632。
而 IDT 对应的描述符说明如下(可参考 中断描述符表):
由上可以看出,这是一个陷阱门。其段选择符为:0x08,其偏移值为:0x7632(30258)。
结合下图的调用说明,
可知:段选择符为 0x08 指向内核代码(参见上面 内核CS段),其段基址为 0x00。因此其指向 0x7632 处。从 System.map 文件可以看到此处正是 system_call 函数。
2、 system_call 函数
其位于 kernel/system_call.s 中。
系统调用 0x80 会导致 CPU 硬件自动将 ss、esp、eflags、cs、eip 的值压栈。系统调用进入可参考 系统调用进入
# 错误的系统调用号 .align 2 # 内存 4 字节对齐 bad_sys_call: movl $-1,%eax # eax 中置 -1,退出中断 iret # 重新执行调度程序入口。调度程序 schedule 在(kernel/sched.c,104) # 当调度程序 schedule() 返回时就从 ret_from_sys_call 处(101行)继续执行 .align 2 reschedule: pushl $ret_from_sys_call # 将 ret_from_sys_call 的地址入栈 jmp schedule #### int 0x80 --linux 系统调用入口点(调用中断 int 0x80,eax 中是调用号)。 .align 2 system_call: cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在 eax 中置-1 并退出。 ja bad_sys_call push %ds # 保存原段寄存器值。 push %es push %fs # 一个系统调用最多可带有 3 个参数,也可以不带参数。下面入栈的 ebx、ecx 和 edx 中放着系统 # 调用相应 C 语言函数(见第 94 行)的调用参数。这几个寄存器入栈的顺序是由 GNU GCC 规定的, # ebx 中可存放第 1 个参数,ecx 中存放第 2 个参数,edx 中存放第 3 个参数。 # 系统调用语句可参见头文件 include/unistd.h 中第 133 至 183 行的系统调用宏。 pushl %edx pushl %ecx # push %ebx, %ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds # ds,es 指向内核数据段(全局描述符表中数据段描述符)。 mov %dx,%es # fs 指向局部数据段(局部描述符表中数据段描述符),即指向执行本次系统调用的用户程序的数据段。 #注意,在 Linux 0.11 中内核给任务分配的代码和数据内存段是重叠的,它们的段基址和段限长相同。 # 参见 fork.c 程序中 copy_mem() 函数。 mov1 $0x17, %edx # fs points to local data space mov %dx,%fs # 下面这句操作数的含义是:调用地址=[_sys_call_table + %eax * 4]。参见程序后的说明。 # sys_call_table[]是一个指针数组,定义在 include/linux/sys.h 中。该指针数组中设置了 # 所有 72 个系统调用 C 处理函数的地址。 call *sys_call_table(,%eax,4) # 间接调用指定功能 C 函数。 95: pushl %eax # 把系统调用返回值入栈。 # 下面 96-100 行查看当前任务的运行状态。如果不在就结状态(state 不等于 0)就去执行调度 #程序。如果该任务在就绪状态,但其时间片已用完(counter = 0),则也去执行调度程序。 #例如当后台进程组中的进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程。 # 会收到 SIGTTIN 或 SIGTTOU 信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻。 # 返回。 movl _current, %eax #取当前任务(进程)数据结构地址到 eax。 cmpl $0,state(%eax) # state jne reschedule cmpl $0,counter(%eax) # counter je reschedule # 以下这段代码执行从系统调用 C 函数返回后,对信号进行识别处理。其他中断服务程序退出时也。 # 将跳转到这里进行处理后才退出中断过程,例如后面 131 行上的处理器出错中断 int 16。 101: ret_from_sys_call: # 首先判别当前任务是否是初始任务 task0,如果是则不必对其进行信号量方面的处理,直接返回。 # 103 行上的_task 对应 C 程序中的 task 数组,直接引用 task 相当于引用 task[0]。 movl current,%eax # task[0] cannot have signals cmpl task,%eax je 3f # 向前(forward)跳转到标号 3 处退出中断处理。 # 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。 # 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是否, # 为用户代码段的选择符 0x000f(RPL=3,局部表,第 1 个段(代码段))来判断是否为用户任务。如 # 果不是则说明是某个中断服务程序跳转到第 101 行的,于是跳转退出中断程序。如果原堆栈段选择。 #符不为 0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出。 cmpw $0x0f,CS(%esp) # was old code segment supervisor ? jne 3f cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? jne 3f # 下面这段代码( 109-120 )用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32 位, # 每位代表 1 种信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值 # 最小的信号值,再把原信号位图中该信号对应的位复位(置 0),最后将该信号值作为参数之一调 # 用 do_signal()。 do_signal() 在(kernel/signal.c,82)中,其参数包括 13 个入栈的信息。 movl signal(%eax),%ebx #取信号位图放入ebx中, 每 1 位代表 1 种信号,共 32 个信号。 movl blocked(%eax),%ecx #取阻塞(屏蔽)信号位图放入ecx中。 notl %ecx # 每位取反。 andl %ebx,%ecx #获得许可的信号位图。 bsfl %ecx,%ecx # 从低位(位 0)开始扫描位图,看是否有 1 的位, # 若有,则 ecx 保留该位的偏移值(即第几位 0-31)。 je 3f # 如果没有信号则向前跳转退出。 btrl %ecx,%ebx # 复位该信号(ebx 含有原 signal 位图)。 movl %ebx,signal(%eax) # 重新保存 signal 位图信息到 current->signal 中。 incl %ecx # 将信号调整为从 1 开始的数(1-32)。 pushl %ecx # 信号值入栈作为调用 do_signal 的参数之一 call do_signal # 调用 C 函数信号处理程序(kernel/signal.c,82) popl %eax # 弹出入栈的信号值。 3: popl %eax # eax 中含有第 95 行入栈的系统调用返回值。 popl %ebx popl %ecx popl %edx pop %fs pop %es pop %ds iret
函数中 call *sys_call_table(,%eax,4) 实际调用的就是 sys_fork 函数。其也定义在当前文件中:
.align 2 sys_fork: call find_empty_process testl %eax,%eax # 在eax中返回进程号pid。若返回负数则退出 js 1f push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call copy_process addl $20,%esp # 丢弃这里所有压栈内容,即上面压入的 gs、esi、edi、ebp、eax 1: ret
sys_fork 函数会调用 find_empty_process 函数找到一个空闲的任务,并返回进程号。然后调用 copy_process 拷贝父进程信息。
2.1 copy_process 函数
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, long ebx,long ecx,long edx, long fs,long es,long ds, long eip,long cs,long eflags,long esp,long ss) { // ... }
GCC 中函数调用的参数逆次压入栈中(参考C 与汇编程序的相互调用 ,3.2 copy_process 函数 ),即最后压入变量 nr。copy_process 函数参数依次对应 %eax,%ebp,%edi,%esi,%gs,none(调用 sys_fork 函数时压入栈的返回地址),%ebx,%ecx,%edx,%fs,%es,%ds,eip,cs,eflags,esp,ss。
2.2 设置进程 1 的分页管理
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, long ebx,long ecx,long edx, long fs,long es,long ds, long eip,long cs,long eflags,long esp,long ss) { // ... if (copy_mem(nr,p)) { // 设置子进程的代码段、数据段及创建、复制子进程的第一个页表 task[nr] = NULL; free_page((long) p); return -EAGAIN; } // ... }
copy_men 函数在 kernel/fork.c 中。参考 3.2.1 copy_mem 函数
int copy_mem(int nr,struct task_struct * p) { unsigned long old_data_base,new_data_base,data_limit; unsigned long old_code_base,new_code_base,code_limit; // 取子进程的代码、数据段限长,跟踪两者都为:655360 code_limit=get_limit(0x0f); data_limit=get_limit(0x17); // 获取父进程(现在为进程 0)的代码段、数据段基址, // 跟踪两者都为 0 old_code_base = get_base(current->ldt[1]); old_data_base = get_base(current->ldt[2]); if (old_data_base != old_code_base) panic("We don't support separate I&D"); if (data_limit < code_limit) panic("Bad data_limit"); new_data_base = new_code_base = nr * 0x4000000; p->start_code = new_code_base; set_base(p->ldt[1],new_code_base); // 设置子进程代码段基址 set_base(p->ldt[2],new_data_base); // 设置子进程数据段基址 if (copy_page_tables(old_data_base,new_data_base,data_limit)) { printk("free_page_tables: from copy_mem\n"); free_page_tables(new_data_base,data_limit); return -ENOMEM; } return 0; }
copy_page_tables 函数请参考 3.2.1.1 copy_page_tables 函数
3、系统调用返回
sys_fork 返回后,其栈中数据如下:
%ebx,%ecx,%edx,%fs,%es,%ds,eip,cs,eflags,esp,ss。
# kernel/system_call.s 95: pushl %eax # 把系统调用返回值入栈。 # 下面 96-100 行查看当前任务的运行状态。如果不在就结状态(state 不等于 0)就去执行调度 #程序。如果该任务在就绪状态,但其时间片已用完(counter = 0),则也去执行调度程序。 #例如当后台进程组中的进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程。 # 会收到 SIGTTIN 或 SIGTTOU 信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻。 # 返回。 movl _current, %eax #取当前任务(进程)数据结构地址到 eax。 cmpl $0,state(%eax) # state jne reschedule cmpl $0,counter(%eax) # counter je reschedule # 以下这段代码执行从系统调用 C 函数返回后,对信号进行识别处理。其他中断服务程序退出时也。 # 将跳转到这里进行处理后才退出中断过程,例如后面 131 行上的处理器出错中断 int 16。 101: ret_from_sys_call: # 首先判别当前任务是否是初始任务 task0,如果是则不必对其进行信号量方面的处理,直接返回。 # 103 行上的_task 对应 C 程序中的 task 数组,直接引用 task 相当于引用 task[0]。 movl current,%eax # task[0] cannot have signals cmpl task,%eax je 3f # 向前(forward)跳转到标号 3 处退出中断处理。 # 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。 # 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。这里比较选择符是否, # 为用户代码段的选择符 0x000f(RPL=3,局部表,第 1 个段(代码段))来判断是否为用户任务。如 # 果不是则说明是某个中断服务程序跳转到第 101 行的,于是跳转退出中断程序。如果原堆栈段选择。 #符不为 0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出。 cmpw $0x0f,CS(%esp) # was old code segment supervisor ? jne 3f cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? jne 3f # 下面这段代码( 109-120 )用于处理当前任务中的信号。首先取当前任务结构中的信号位图(32 位, # 每位代表 1 种信号),然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值 # 最小的信号值,再把原信号位图中该信号对应的位复位(置 0),最后将该信号值作为参数之一调 # 用 do_signal()。 do_signal() 在(kernel/signal.c,82)中,其参数包括 13 个入栈的信息。 movl signal(%eax),%ebx #取信号位图放入ebx中, 每 1 位代表 1 种信号,共 32 个信号。 movl blocked(%eax),%ecx #取阻塞(屏蔽)信号位图放入ecx中。 notl %ecx # 每位取反。 andl %ebx,%ecx #获得许可的信号位图。 bsfl %ecx,%ecx # 从低位(位 0)开始扫描位图,看是否有 1 的位, # 若有,则 ecx 保留该位的偏移值(即第几位 0-31)。 je 3f # 如果没有信号则向前跳转退出。 btrl %ecx,%ebx # 复位该信号(ebx 含有原 signal 位图)。 movl %ebx,signal(%eax) # 重新保存 signal 位图信息到 current->signal 中。 incl %ecx # 将信号调整为从 1 开始的数(1-32)。 pushl %ecx # 信号值入栈作为调用 do_signal 的参数之一 call do_signal # 调用 C 函数信号处理程序(kernel/signal.c,82) popl %eax # 弹出入栈的信号值。 3: popl %eax # eax 中含有第 95 行入栈的系统调用返回值。 popl %ebx popl %ecx popl %edx pop %fs pop %es pop %ds iret
4、寄存器信息
4.1 进程 0
其 ldt ,tss 不变。
4.1.1 内核态信息
4.1.2 用户态信息
从用户态切换到内核态,处理器从当前执行任务的 TSS 段中得到异常处理过程(内核态)使用的堆栈的段选择符和栈指针(例如 tss.ss0、tss.esp0)。然后处理器会把被中断程序(或任务)的 栈选择符 和 栈指针 压入新栈中。接着处理器会把 EFLAGS、CS 和 EIP 寄存器的当前值也压入新栈中。
结合上面可知,其 cs,ss,ds,es 发生了改变。除了系统调用时处理器会自动改变 ss、es 的值。其它值是在 kernel/system_call.s 文件中 system_call 函数中改变的,其改变了ds,es,以及 fs 的值 。
movl $0x10,%edx # set up ds,es to kernel space mov %dx,%ds # ds,es 指向内核数据段(全局描述符表中数据段描述符)。 mov %dx,%es # fs 指向局部数据段(局部描述符表中数据段描述符),即指向执行本次系统调用的用户程序的数据段。 #注意,在 Linux 0.11 中内核给任务分配的代码和数据内存段是重叠的,它们的段基址和段限长相同。 # 参见 fork.c 程序中 copy_mem() 函数。 mov1 $0x17, %edx # fs points to local data space mov %dx,%fs
系统调用进入可参考 系统调用进入,系统调用返回可参考 系统调用返回。
4.1.3 task_struct 中其它字段
4.2 进程 1
4.2.1 初始化时信息:
ldt
tss
4.2.2 用户态信息
通用寄存器和段寄存器
ldt 和 tss 不变。
4.2.3 内核态信息
通用寄存器和段寄存器
ldt 和 tss 不变。
4.2.4 task_struct 中其它字段
四、进程 2
1、调用 fork 创建进程 2
void init(void) { // ... if (!(pid = fork())) { close(0); if (open("/etc/rc", O_RDONLY, 0)) _exit(1); execve("/bin/sh", argv_rc, envp_rc); _exit(2); } // ...
通用寄存器和段寄存器
ldt
tss
task_struct 中其它字段
除了 start_code 其它字段与进程 0 和 进程 1 类似。其中的 ldt, tss 可以参看上图。
2、进程 2 执行 execve 函数
execve 是个系统调用,最终会调用 fs/exec.c 文件中的 do_execve 函数。此时程序处在进程 2 的内核态。其通用寄存器和段寄存器未发生改变。详细可以参考:Linux0.11 execve函数(六)
ldt
ldt[2] 值为:0x08c0f300:3fff,此 LDT 描述符图释如下:详细可参考:4、段描述符
由上图可知:其段基址为 128MB,处在用户态,段限长为 64MB。
tss
task_struct 中其它字段
进程 2 相比进程 0,1,其现在 end_code,end_data,brk, excutable 都有值了。这些值都是在 do_execve 函数中赋予的。
中断
task0
信息被压入到 tss 中。
task1