kernel源码(九)main.c


0 __asm__

参考1:中的嵌入式汇编部分

参考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 源码

/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */

#define __LIBRARY__
#include 
#include 

/*
 * 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 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#include 

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 
#include 

接下来,定义了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

下面引入了一些头文件

#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 

#include 

下面定义了一个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开始的位置放入了一些数据(),这里我们开始使用这些数据

/*
 * 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时间,秒
        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);
    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
 * 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(); //调度程序的初始化
    buffer_init(buffer_memory_end); //缓冲区管理的初始化
    hd_init();//硬盘初始化
    floppy_init();//软盘初始化
    sti();//打开中断
    move_to_user_mode();//在system.h中定义的宏,作用是初始化数据段、附加段
    if (!fork()) {        //fork是前面定义的宏,作用是新建子进程
        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(); //这一段的意思是,定义了一个死循环,每循环一次,执行一次pause,如果此时有其他进程在等待,将获取cpu时间片执行;如果没有其他进程,将一直执行for循环;这里就是那个idel进程
}

其中下面这些初始化是非常重要的部分,后面博客会一一讲到。

    mem_init(main_memory_start,memory_end); //初始化主内存
    trap_init(); //中断门初始化
    blk_dev_init(); //块设备初始化
    chr_dev_init(); //字符设备初始化
    tty_init(); //tty的初始化
    time_init(); //时间的初始化
    sched_init(); //调度程序的初始化
    buffer_init(buffer_memory_end); //缓冲区管理的初始化
    hd_init();//硬盘初始化
    floppy_init();//软盘初始化

下面程序,创建一个init进程

    if (!fork()) {        //fork是前面定义的宏,作用是新建子进程
        init();
    }

我们看一下init()函数

void init(void)
{
    int pid,i;

    setup((void *) &drive_info); //设置硬盘,在hd.c中定义
    (void) open("/dev/tty0",O_RDWR,0); //以只读的方式打开/dev/tty0
    (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())) { //再创建一个子进程(任务2)
        close(0);//关闭描述符0
        if (open("/etc/rc",O_RDONLY,0))//打开/etc/rc文件
            _exit(1);
        execve("/bin/sh",argv_rc,envp_rc);//执行/bin/sh
        _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() */
}

我们对main.c程序讲解了一个框架,里面一些细节我们会在接下来的文章中逐步深化。