kernel源码(九)main.c
0 __asm__
参考1:https://www.cnblogs.com/zhenjingcool/p/15925494.html中的嵌入式汇编部分
参考2:https://blog.csdn.net/yt_42370304/article/details/84982864
00 系统调用int 0x80
在保护模式下,内核采用中断的方式实现系统调用,中断向量号为0x80;这是操作系统在用户态访问内核态唯一的途径。
具体哪种系统调用,是由eax中的值决定的,称为功能号,功能号在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 };
这里定义了一个数组,数组中的元素是相关系统调用函数地址,数组下标就是功能号,比如我们调用int 0x80时eax中存放2的话,就是sys_fork系统调用。
系统调用的一般过程:
目前各个发行版中系统调用在体系结构中的位置:
1、用户空间的某一个函数,比如我们在c程序写的系统应用中需要创建一个进程,我们会调用glibc为我们提供的库函数fork()。在linux0.11内核中没有库函数,内核为我们提供的fork()函数起到同样的作用
2、glibc中的fork()函数中会执行汇编命令int 0x80发起系统调用,因为执行了int 0x80,因而从用户态转到内核态。
3、系统调用int 0x80本质上是一个软中断,其对应一个中断处理程序,这个中断处理程序是system_call.s中的_system_call标号。(为什么中断处理程序是system_call.s后面解释)
4、_system_call中做现场保护,然后根据eax中的系统调用号,查找_sys_call_table中对应的系统调用。并调用sys_fork函数。
5、系统调用完成后,检查任务结构体中的信号位图,并处理信号
为什么int 0x80中断处理程序是system_call.s?下面进行解释
在main.c中执行初始化时,其中有一步是sched_init();,这个函数在sched.c中定义的
void sched_init(void) { int i; struct desc_struct * p; if (sizeof(struct sigaction) != 16) panic("Struct sigaction MUST be 16 bytes"); set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss)); set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt)); p = gdt+2+FIRST_TSS_ENTRY; for(i=1;i<NR_TASKS;i++) { task[i] = NULL; p->a=p->b=0; p++; p->a=p->b=0; p++; } /* Clear NT, so that we won't have troubles with that later on */ __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl"); ltr(0); lldt(0); outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */ outb_p(LATCH & 0xff , 0x40); /* LSB */ outb(LATCH >> 8 , 0x40); /* MSB */ set_intr_gate(0x20,&timer_interrupt); outb(inb_p(0x21)&~0x01,0x21); set_system_gate(0x80,&system_call); }
最下面一行set_system_gate(0x80,&system_call);,在这里设置了中断号0x80对应的中断处理程序是system_call。
1 源码
/* * linux/init/main.c * * (C) 1991 Linus Torvalds */ #define __LIBRARY__ #include <unistd.h> #include <time.h> /* * we need this inline - forking from kernel space will result * in NO COPY ON WRITE (!!!), until an execve is executed. This * is no problem, but for the stack. This is handled by not letting * main() use the stack at all after fork(). Thus, no function * calls - which means inline code for fork too, as otherwise we * would use the stack upon exit from 'fork()'. * * Actually only pause and fork are needed inline, so that there * won't be any messing with the stack from main(), but we define * some others too. */ static inline _syscall0(int,fork) static inline _syscall0(int,pause) static inline _syscall1(int,setup,void *,BIOS) static inline _syscall0(int,sync) #include <linux/tty.h> #include <linux/sched.h> #include <linux/head.h> #include <asm/system.h> #include <asm/io.h> #include <stddef.h> #include <stdarg.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <linux/fs.h> static char printbuf[1024]; extern int vsprintf(); extern void init(void); extern void blk_dev_init(void); extern void chr_dev_init(void); extern void hd_init(void); extern void floppy_init(void); extern void mem_init(long start, long end); extern long rd_init(long mem_start, int length); extern long kernel_mktime(struct tm * tm); extern long startup_time; /* * This is set up by the setup-routine at boot-time */ #define EXT_MEM_K (*(unsigned short *)0x90002) #define DRIVE_INFO (*(struct drive_info *)0x90080) #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) /* * Yeah, yeah, it's ugly, but I cannot find how to do this correctly * and this seems to work. I anybody has more info on the real-time * clock I'd be interested. Most of this was trial and error, and some * bios-listing reading. Urghh. */ #define CMOS_READ(addr) ({ \ outb_p(0x80|addr,0x70); \ inb_p(0x71); \ }) #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10) static void time_init(void) { struct tm time; do { time.tm_sec = CMOS_READ(0); time.tm_min = CMOS_READ(2); time.tm_hour = CMOS_READ(4); time.tm_mday = CMOS_READ(7); time.tm_mon = CMOS_READ(8); time.tm_year = CMOS_READ(9); } while (time.tm_sec != CMOS_READ(0)); BCD_TO_BIN(time.tm_sec); BCD_TO_BIN(time.tm_min); BCD_TO_BIN(time.tm_hour); BCD_TO_BIN(time.tm_mday); BCD_TO_BIN(time.tm_mon); BCD_TO_BIN(time.tm_year); time.tm_mon--; startup_time = kernel_mktime(&time); } static long memory_end = 0; static long buffer_memory_end = 0; static long main_memory_start = 0; struct drive_info { char dummy[32]; } drive_info; void main(void) /* This really IS void, no error here. */ { /* The startup routine assumes (well, ...) this */ /* * Interrupts are still disabled. Do necessary setups, then * enable them */ ROOT_DEV = ORIG_ROOT_DEV; drive_info = DRIVE_INFO; memory_end = (1<<20) + (EXT_MEM_K<<10); memory_end &= 0xfffff000; if (memory_end > 16*1024*1024) memory_end = 16*1024*1024; if (memory_end > 12*1024*1024) buffer_memory_end = 4*1024*1024; else if (memory_end > 6*1024*1024) buffer_memory_end = 2*1024*1024; else buffer_memory_end = 1*1024*1024; main_memory_start = buffer_memory_end; #ifdef RAMDISK main_memory_start += rd_init(main_memory_start, RAMDISK*1024); #endif mem_init(main_memory_start,memory_end); trap_init(); blk_dev_init(); chr_dev_init(); tty_init(); time_init(); sched_init(); buffer_init(buffer_memory_end); hd_init(); floppy_init(); sti(); move_to_user_mode(); if (!fork()) { /* we count on this going ok */ init(); } /* * NOTE!! For any other task 'pause()' would mean we have to get a * signal to awaken, but task0 is the sole exception (see 'schedule()') * as task 0 gets activated at every idle moment (when no other tasks * can run). For task0 'pause()' just means we go check if some other * task can run, and if not we return here. */ for(;;) pause(); } static int printf(const char *fmt, ...) { va_list args; int i; va_start(args, fmt); write(1,printbuf,i=vsprintf(printbuf, fmt, args)); va_end(args); return i; } static char * argv_rc[] = { "/bin/sh", NULL }; static char * envp_rc[] = { "HOME=/", NULL }; static char * argv[] = { "-/bin/sh",NULL }; static char * envp[] = { "HOME=/usr/root", NULL }; void init(void) { int pid,i; setup((void *) &drive_info); (void) open("/dev/tty0",O_RDWR,0); (void) dup(0); (void) dup(0); printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS, NR_BUFFERS*BLOCK_SIZE); printf("Free mem: %d bytes\n\r",memory_end-main_memory_start); if (!(pid=fork())) { close(0); if (open("/etc/rc",O_RDONLY,0)) _exit(1); execve("/bin/sh",argv_rc,envp_rc); _exit(2); } if (pid>0) while (pid != wait(&i)) /* nothing */; while (1) { if ((pid=fork())<0) { printf("Fork failed in init\r\n"); continue; } if (!pid) { close(0);close(1);close(2); setsid(); (void) open("/dev/tty0",O_RDWR,0); (void) dup(0); (void) dup(0); _exit(execve("/bin/sh",argv,envp)); } while (1) if (pid == wait(&i)) break; printf("\n\rchild %d died with code %04x\n\r",pid,i); sync(); } _exit(0); /* NOTE! _exit, not exit() */ }
开头
#define __LIBRARY__ #include <unistd.h> #include <time.h>
接下来,定义了4个内联函数,而且是在unistd.h中的宏定义。
static inline _syscall0(int,fork) static inline _syscall0(int,pause) static inline _syscall1(int,setup,void *,BIOS) static inline _syscall0(int,sync)
我们看一下unistd.h中是如何定义_syscall0的
#define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name)); \ //##为C中的语法,这里表示连接__NR_和name,这里是__NR_fork,这个是在本头文件中定义的宏(#define __NR_fork 2) if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ }
在这个宏定义中,嵌入了汇编代码。
int 0x80:int表示中断,0x80表示系统调用,int 0x80表示发起一个系统调用
"0" (__NR_##name) 为asm的输入,__NR_##name为2,这里意思是把2放入0的位置处,即把2放入eax寄存器中(asm规定,0到9分别表示特定的位置,不明白可以查看:https://blog.csdn.net/yt_42370304/article/details/84982864)
"=a" (__res) 为asm的输出,表示把eax中的内容写到变量__res中。
上面这段代码表示发起一个系统调用,功能号是2,功能号2对应的是fork系统调用。
我们再回过头来看main.c中的这段代码
static inline _syscall0(int,fork) static inline _syscall0(int,pause) static inline _syscall1(int,setup,void *,BIOS) static inline _syscall0(int,sync)
这里定义了4个内联函数,作用分别是系统调用fork、系统调用pause、系统调用setup、系统调用sync。需要注意的是,本文件中定义了这4个系统调用,但是没有在本文件中使用。
下面引入了一些头文件
#include <linux/tty.h> #include <linux/sched.h> #include <linux/head.h> #include <asm/system.h> #include <asm/io.h> #include <stddef.h> #include <stdarg.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <linux/fs.h>
下面定义了一个char数组
static char printbuf[1024];
下面是一些函数,他们的原型分别在不同的c文件中。
extern int vsprintf(); extern void init(void); extern void blk_dev_init(void); extern void chr_dev_init(void); extern void hd_init(void); extern void floppy_init(void); extern void mem_init(long start, long end); extern long rd_init(long mem_start, int length); extern long kernel_mktime(struct tm * tm); extern long startup_time;
我们在setup.s中在0x90000开始的位置放入了一些数据(https://www.cnblogs.com/zhenjingcool/p/15944047.html),这里我们开始使用这些数据
/* * This is set up by the setup-routine at boot-time */ #define EXT_MEM_K (*(unsigned short *)0x90002) //0x90002处存放的是扩展内存大小。这里我们把扩展内存大小赋值给宏EXT_MEM_K #define DRIVE_INFO (*(struct drive_info *)0x90080) //第一个硬盘的信息 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) //根设备信息
下面代码是读取cmos实时时钟信息。注意这里说的端口其实就是一个内存地址
#define CMOS_READ(addr) ({ \ outb_p(0x80|addr,0x70); \ //0x70是写端口号,0x80|addr 是要读取的CMOS 内存地址 inb_p(0x71); \ //0x71 是读端口号。 })
outb_p是一个宏,在io.h中定义,汇编中outb指令是向特定io端口写一字节数据。inb是特定io端口向源头读入1字节数据。
#define outb(value,port) \ __asm__ ("outb %%al,%%dx"::"a" (value),"d" (port)) //无输出,输入:value写入eax,port写入edx。这个嵌入汇编意思是把value发到port端口 #define inb(port) ({ \ unsigned char _v; \ __asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \ //从port端口读取1字节数据放入eax,并把eax值存入_v;返回_v _v; \ }) #define outb_p(value,port) \ __asm__ ("outb %%al,%%dx\n" \ "\tjmp 1f\n" \ //jmp 1f意思是跳转到标号1处,方向是向前跳转(因为这里有两个标号1,所以要区分向前还是向后) "1:\tjmp 1f\n" \ "1:"::"a" (value),"d" (port)) #define inb_p(port) ({ \ unsigned char _v; \ __asm__ volatile ("inb %%dx,%%al\n" \ "\tjmp 1f\n" \ "1:\tjmp 1f\n" \ "1:":"=a" (_v):"d" (port)); \ _v; \ })
下面代码定义了一个宏,作用是把BCD码转换为二进制的值
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
下面代码实现时间初始化。
static void time_init(void) { struct tm time; //在time.h中定义的 do { time.tm_sec = CMOS_READ(0); //读取cmos时间,秒.在教材7.1.3.1小节有相关介绍,cmos在指定位置存放了当前时分秒。通过0x70设置地址,cmos会把读取后的信息放到0x71,。我们读取0x71就获取到值 time.tm_min = CMOS_READ(2); //分钟 time.tm_hour = CMOS_READ(4); //小时 time.tm_mday = CMOS_READ(7); //天 time.tm_mon = CMOS_READ(8); //月 time.tm_year = CMOS_READ(9); //年 } while (time.tm_sec != CMOS_READ(0)); //如果读取时间超过1秒,则重新读取。保证误差在1秒之内 BCD_TO_BIN(time.tm_sec);//cmos中的值是bcd码,这里对bcd码进行转码 BCD_TO_BIN(time.tm_min); BCD_TO_BIN(time.tm_hour); BCD_TO_BIN(time.tm_mday); BCD_TO_BIN(time.tm_mon); BCD_TO_BIN(time.tm_year); time.tm_mon--; //因为tm_mon从1-12,这里做减1操作 startup_time = kernel_mktime(&time); //创建时间,赋值给start_time }
定义了一个结构体
struct drive_info { char dummy[32]; } drive_info;
下面是main函数,在介绍head.s时我们讲过,head.s中设置了调用c中main函数的入口,就是指的这里(注意:汇编调用c中的函数时要加_,即_main)
下面的代码需要对照这个图查看脉络将会更清晰。
系统内存划分为内核程序,缓存,虚拟盘,主存储区这几部分,见下图
void main(void) /* This really IS void, no error here. */ { /* The startup routine assumes (well, ...) this */ /* * Interrupts are still disabled. Do necessary setups, then 此时还处于关中断状态,关中断是在setup.s中关闭的,从那以后到目前为止还没有打开。 * enable them */ ROOT_DEV = ORIG_ROOT_DEV; //ROOT_DEV是在其他文件中定义的宏,讲到时我们再回来看这地方 drive_info = DRIVE_INFO; //DRIVE_INFO前面定义的宏,这里赋值给driver_info memory_end = (1<<20) + (EXT_MEM_K<<10); //计算总的内存大小,单位为字节,1左移20位表示1M基本内存地址,EXT_MEM_K是前面定义的宏表示扩展内存大小(kb),这里左移10位转换为字节 memory_end &= 0xfffff000; //因为内存是分页的,1页是4kb,这里把最后不足1页的内存去掉 if (memory_end > 16*1024*1024) //如果内存大于16M, memory_end = 16*1024*1024; if (memory_end > 12*1024*1024) //如果内存大于12M,缓冲区大小设置为4M buffer_memory_end = 4*1024*1024; else if (memory_end > 6*1024*1024) //如果内存大于6M,缓冲区大小设置为2M buffer_memory_end = 2*1024*1024; else buffer_memory_end = 1*1024*1024; main_memory_start = buffer_memory_end; //缓冲区后面是主内存的地址 #ifdef RAMDISK //如果定义了虚拟内存,则主内存开始位置还要往后延一点 main_memory_start += rd_init(main_memory_start, RAMDISK*1024); #endif mem_init(main_memory_start,memory_end); //初始化主内存 trap_init(); //中断门初始化 blk_dev_init(); //块设备初始化 chr_dev_init(); //字符设备初始化 tty_init(); //tty的初始化 time_init(); //时间的初始化 sched_init(); //调度程序的初始化,这里会初始化任务0。因为调度程序依赖于时间片中断,在中断打开之前是不会发生调度的,任务0会等待开中断后由调度程序调度执行?解释:在执行了下面的sti()函数开中断后,接下来将由调度程序决定CPU上下文执行哪个进程,此时只有一个进程:任务0,因此此时任务0开始执行。 buffer_init(buffer_memory_end); //缓冲区管理的初始化 hd_init();//硬盘初始化 floppy_init();//软盘初始化 sti();//打开中断 move_to_user_mode();//在system.h中定义的宏,作用是初始化数据段、附加段。任务0是如何运行的?通过在堆栈中设置的参数,利用中断返回指令启动任务0执行 if (!fork()) { //fork是前面定义的宏,作用是新建子进程,这里创建的是任务1。fork()调用会产生和父进程一样的进程描述符,也就是说父进程和子进程的代码段完全一样,执行相同的代码,对于父进程来说,fork()调用返回进程号pid,对于子进程来说,fork()调用返回0.因此这里if判断条件决定了父进程和子进程不同的代码执行逻辑。此处 子进程中if条件才为真,执行init()函数,父进程不会执行init()函数。 init();//在init中会初始化标准输入stdin、标准输出stdout。然后重新打开一个sh交互程序,也就是我们看到的那个黑框,只要我们不exit,黑框一直存在,也就是这个进程n一直运行,当我们关闭了黑框或者执行exit命令,这个进程n才结束。当然我们可以打开多个交互式sh窗口,也就是打开多个sh进程。 } /* * NOTE!! For any other task 'pause()' would mean we have to get a * signal to awaken, but task0 is the sole exception (see 'schedule()') * as task 0 gets activated at every idle moment (when no other tasks * can run). For task0 'pause()' just means we go check if some other * task can run, and if not we return here. */ for(;;) pause(); //这一段的意思是,定义了一个死循环,每循环一次,执行一次pause,如果此时有其他进程在等待,将获取cpu时间片执行;如果没有其他进程,将一直执行for循环;这里就是那个idel进程。这一行代码只有任务0才能执行到,任务1进入到上面的if分支中了,也就是说如果没有其他进程,任务0会一直调用pause() }
其中下面这些初始化是非常重要的部分,后面博客会一一讲到。
mem_init(main_memory_start,memory_end); //初始化主内存 trap_init(); //中断门初始化 blk_dev_init(); //块设备初始化 chr_dev_init(); //字符设备初始化 tty_init(); //tty的初始化 time_init(); //时间的初始化 sched_init(); //调度程序的初始化,这里会创建任务0,并且移动到任务0中执行 buffer_init(buffer_memory_end); //缓冲区管理的初始化 hd_init();//硬盘初始化 floppy_init();//软盘初始化
下面程序,创建任务1,并执行init函数
if (!fork()) { //fork是前面定义的宏,作用是新建子进程 init(); }
定义了一些字符数组作为sh程序的参数,后面会用到
static char * argv_rc[] = { "/bin/sh", NULL }; static char * envp_rc[] = { "HOME=/", NULL }; static char * argv[] = { "-/bin/sh",NULL }; static char * envp[] = { "HOME=/usr/root", NULL };
我们看一下init()函数。
init函数有如下3个功能:1 安装根文件系统;2 运行系统初始资源配置文件/etc/rc中的命令;3 执行用户登陆shell程序
void init(void) { int pid,i; setup((void *) &drive_info); //设置硬盘,在hd.c中定义 (void) open("/dev/tty0",O_RDWR,0); //第一个参数表示要读写的文件名,第二个参数表示已读写方式打开,第三个参数表示权限标志。以读写的方式打开/dev/tty0。它对应终端控制台。由于这是第一次打开文件操作,因此产生的文件描述符(文件句柄)是0,也就是标准输入句柄stdin (void) dup(0); //复制文件描述符,产生1号句柄,即stdout标准输出设备。关于这三行代码的解释,可参考https://blog.csdn.net/m0_53157173/article/details/127784664 (void) dup(0); //复制文件描述符,产生2号句柄,即stderr标准错误输出设备。最终0号句柄指向/dev/tty0。1,2句柄指向0号句柄。这也是为什么默认输入和输出都是控制台终端的原因 printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS, NR_BUFFERS*BLOCK_SIZE); printf("Free mem: %d bytes\n\r",memory_end-main_memory_start); if (!(pid=fork())) { //再创建一个子进程(任务2),同样的道理,对于父进程fork()函数返回pid,对于子进程fork()函数返回0;此处只有子进程(也就是任务2)会进入if分支 close(0);//关闭文件描述符0,并立即打开文件/etc/rc,从而把标准输入stdin定向到/etc/rc文件上。这样所有的标准输入数据都将从该文件中读取。 if (open("/etc/rc",O_RDONLY,0))//打开/etc/rc文件 _exit(1); execve("/bin/sh",argv_rc,envp_rc);//内核以非交互形式执行/bin/sh,从而实现执行/etc/rc文件中的命令,当该文件中的命令执行完毕后,/bin/sh就会立即退出,因此进程2也就随之结束。 _exit(2); //退出子进程 } if (pid>0) //如果子进程创建成功,这里是父进程(任务1)执行的分支,只有任务1才能进入if里面 while (pid != wait(&i)) //等待子进程(任务2)结束,任务2以非交互方式运行sh程序,执行/etc/rc里面的命令,运行完则任务2结束 /* nothing */; while (1) { //如果子进程(任务2)结束,才轮到这里执行 if ((pid=fork())<0) { //再创建一个进程 printf("Fork failed in init\r\n"); continue; } if (!pid) { //子进程才会执行这个if分支。在子进程中关闭 close(0);close(1);close(2);//在这个新创建的子进程中,关闭所有以前遗留下来的句柄(stdin,stdout,stderr) setsid();//新创建一个会话 (void) open("/dev/tty0",O_RDWR,0);//重新打开/dev/tty0作为stdin (void) dup(0);//stdout (void) dup(0);//stderr _exit(execve("/bin/sh",argv,envp));//再次执行/bin/sh程序,但这次执行所传递的参数和前面不一样,参数中包含了-/bin/sh,注意前面的横线-,表示登录shell,和前面非交互式shell不同。这个登录shell将一直运行,除非我们执行exit命令退出。 } while (1) //这里父进程while循环判断某一子进程是否退出,如果退出则打印"XXX退出"等信息,然后继续等待其他登录shell(子进程)退出。此外wait()函数也处理孤儿进程,把其父进程号改为1号进程。 if (pid == wait(&i)) break; printf("\n\rchild %d died with code %04x\n\r",pid,i); sync(); //刷新缓冲区 } _exit(0); /* NOTE! _exit, not exit() */ }
我们对main.c程序讲解了一个框架,里面一些细节我们会在接下来的文章中逐步深化。