Linux 0.11 系统调用的实现机制

2023-05-16

Linux 0.11 系统调用的实现机制

一、系统调用概述

    系统调用本质上是一种中断,中断号为0x80,即128号中断。通常我们使用的是库函数,而不是直接使用系统调用,这主要是因为库函数一般都是规定好的,是可以移植的。而系统调用的具体子调用号可能会发生改变,不同平台可能不一样,写出来的程序难以移植。触发系统调用,会进入内核态,并调用绑定的处理函数。内核开发人员必须考虑如何将用户空间的参数传递给内核,同时怎么把系统调用的结果传回用户空间。0.11版本的内核使用寄存器来传递参数和传递返回值,同时用来传递调用号,如read,write的对应的调用号。这个调用号实际上对应一张系统调用表的下标,该系统调用表是一个数组,保存内核态下具体调用函数的入口地址。如果传递的是一个用户态下的地址,则使用指向用户数据段的寄存器fs来对应用户数据区,从而实现对用户数据进行读写。大部分库函数都是用int 0x80实现的,但不是所有库函数都需要系统调用。内核在移入用户态时,需要在用户态下使用部分内核库函数,启动init进程,shell进程。然而库函数并不是内核的一部分,需要交给上一层来实现。

二、内核库函数的实现

2.1 unistd.h文件

    在include/unistd.h(p380)中,定义了72个系统调用号:

#define __NR_setup 0 /* used only by init, to get system going */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
#define __NR_creat 8
#define __NR_link 9
#define __NR_unlink 10
#define __NR_execve 11
#define __NR_chdir 12
#define __NR_time 13
#define __NR_mknod 14
#define __NR_chmod 15
#define __NR_chown 16
#define __NR_break 17
#define __NR_stat 18
#define __NR_lseek 19
#define __NR_getpid 20
#define __NR_mount 21
#define __NR_umount 22
#define __NR_setuid 23
#define __NR_getuid 24
#define __NR_stime 25
#define __NR_ptrace 26
#define __NR_alarm 27
#define __NR_fstat 28
#define __NR_pause 29
#define __NR_utime 30
#define __NR_stty 31
#define __NR_gtty 32
#define __NR_access 33
#define __NR_nice 34
#define __NR_ftime 35
#define __NR_sync 36
#define __NR_kill 37
#define __NR_rename 38
#define __NR_mkdir 39
#define __NR_rmdir 40
#define __NR_dup 41
#define __NR_pipe 42
#define __NR_times 43
#define __NR_prof 44
#define __NR_brk 45
#define __NR_setgid 46
#define __NR_getgid 47
#define __NR_signal 48
#define __NR_geteuid 49
#define __NR_getegid 50
#define __NR_acct 51
#define __NR_phys 52
#define __NR_lock 53
#define __NR_ioctl 54
#define __NR_fcntl 55
#define __NR_mpx 56
#define __NR_setpgid 57
#define __NR_ulimit 58
#define __NR_uname 59
#define __NR_umask 60
#define __NR_chroot 61
#define __NR_ustat 62
#define __NR_dup2 63
#define __NR_getppid 64
#define __NR_getpgrp 65
#define __NR_setsid 66
#define __NR_sigaction 67
#define __NR_sgetmask 68
#define __NR_ssetmask 69
#define __NR_setreuid 70
#define __NR_setregid 71

    显然,exit1号,fork2号,read3号,write4号。setup仅能调用一次。Linux 0.11可以使用的系统调用是72个。

同时,该文件还定义了几个宏:

#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; \
}
 
#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
 
#define _syscall2(type,name,atype,a,btype,b) \
type name(atype a,btype b) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
 
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}

    这些宏将用具体的函数名、返回值、参数,扩展成一个库函数。其中eax保存的子调用号,ebx是第一个参数,ecx是第二个参数,edx是第三个参数。返回值保存在eax中。使用int 0x80启用系统调用,调用结束之后,执行的下一步骤是检查返回值,没有出错则将返回值返回,出错则将返回值的正值赋给errno,返回-1。这是我们为什么在函数出错时,查看errno的原因,而且必须立即查看。最多可以传递3个参数。 

    该文件还声明了一些库函数的原型:

int access(const char * filename, mode_t mode);
int acct(const char * filename);
int alarm(int sec);
int brk(void * end_data_segment);
void * sbrk(ptrdiff_t increment);
int chdir(const char * filename);
int chmod(const char * filename, mode_t mode);
int chown(const char * filename, uid_t owner, gid_t group);
int chroot(const char * filename);
int close(int fildes);
int creat(const char * filename, mode_t mode);
int dup(int fildes);
int execve(const char * filename, char ** argv, char ** envp);
int execv(const char * pathname, char ** argv);
int execvp(const char * file, char ** argv);
int execl(const char * pathname, char * arg0, ...);
int execlp(const char * file, char * arg0, ...);
int execle(const char * pathname, char * arg0, ...);
volatile void exit(int status);
volatile void _exit(int status);
int fcntl(int fildes, int cmd, ...);
int fork(void);
int getpid(void);
int getuid(void);
int geteuid(void);
int getgid(void);
int getegid(void);
int ioctl(int fildes, int cmd, ...);
int kill(pid_t pid, int signal);
int link(const char * filename1, const char * filename2);
int lseek(int fildes, off_t offset, int origin);
int mknod(const char * filename, mode_t mode, dev_t dev);
int mount(const char * specialfile, const char * dir, int rwflag);
int nice(int val);
int open(const char * filename, int flag, ...);
int pause(void);
int pipe(int * fildes);
int read(int fildes, char * buf, off_t count);
int setpgrp(void);
int setpgid(pid_t pid,pid_t pgid);
int setuid(uid_t uid);
int setgid(gid_t gid);
void (*signal(int sig, void (*fn)(int)))(int);
int stat(const char * filename, struct stat * stat_buf);
int fstat(int fildes, struct stat * stat_buf);
int stime(time_t * tptr);
int sync(void);
time_t time(time_t * tloc);
time_t times(struct tms * tbuf);
int ulimit(int cmd, long limit);
mode_t umask(mode_t mask);
int umount(const char * specialfile);
int uname(struct utsname * name);
int unlink(const char * filename);
int ustat(dev_t dev, struct ustat * ubuf);
int utime(const char * filename, struct utimbuf * times);
pid_t waitpid(pid_t pid,int * wait_stat,int options);
pid_t wait(int * wait_stat);
int write(int fildes, const char * buf, off_t count);
int dup2(int oldfd, int newfd);
int getppid(void);
pid_t getpgrp(void);
pid_t setsid(void);

    这也是我们使用系统调用时包含头文件unistd.h的原因。

2.2 include/linux/sys.h文件

    include/linux/sys.h(p407)文件中定义了72个内核态下的系统调用实际调用的函数,都是以sys_开头,以及系统调用指针数组:

extern int sys_setup();
extern int sys_exit();
extern int sys_fork();
extern int sys_read();
extern int sys_write();
extern int sys_open();
extern int sys_close();
extern int sys_waitpid();
extern int sys_creat();
extern int sys_link();
extern int sys_unlink();
extern int sys_execve();
extern int sys_chdir();
extern int sys_time();
extern int sys_mknod();
extern int sys_chmod();
extern int sys_chown();
extern int sys_break();
extern int sys_stat();
extern int sys_lseek();
extern int sys_getpid();
extern int sys_mount();
extern int sys_umount();
extern int sys_setuid();
extern int sys_getuid();
extern int sys_stime();
extern int sys_ptrace();
extern int sys_alarm();
extern int sys_fstat();
extern int sys_pause();
extern int sys_utime();
extern int sys_stty();
extern int sys_gtty();
extern int sys_access();
extern int sys_nice();
extern int sys_ftime();
extern int sys_sync();
extern int sys_kill();
extern int sys_rename();
extern int sys_mkdir();
extern int sys_rmdir();
extern int sys_dup();
extern int sys_pipe();
extern int sys_times();
extern int sys_prof();
extern int sys_brk();
extern int sys_setgid();
extern int sys_getgid();
extern int sys_signal();
extern int sys_geteuid();
extern int sys_getegid();
extern int sys_acct();
extern int sys_phys();
extern int sys_lock();
extern int sys_ioctl();
extern int sys_fcntl();
extern int sys_mpx();
extern int sys_setpgid();
extern int sys_ulimit();
extern int sys_uname();
extern int sys_umask();
extern int sys_chroot();
extern int sys_ustat();
extern int sys_dup2();
extern int sys_getppid();
extern int sys_getpgrp();
extern int sys_setsid();
extern int sys_sigaction();
extern int sys_sgetmask();
extern int sys_ssetmask();
extern int sys_setreuid();
extern int sys_setregid();

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
                          };

    其中,fn_ptr定义在include/linux/sched.h(p401)的第40行:

typedef int (*fn_ptr)();

表示一种int function_name()的函数地址类型,从上述的函数中可以看出并没有对应任何参数,但实际sys_exit等可能不止一个参数,这里主要是为了与调用表相对应。这些系统调用函数与上面系统调用号是一一对应的,内核需要实现的便是这些以sys_开头的函数,开放给用户使用的入口便是int 0x80系统调用

2.3 内核库函数的实现

    在lib下,有多个内核库函数的实现,大部分使用了_syscallx嵌入式宏来实现的,例如lib\write.c(p429),定义了write:

#define __LIBRARY__

#include <unistd.h>

_syscall3(int,write,int,fd,const char *,buf,off_t,count)

显然,宏展开后将变为:

int write(int fd, const char* buf, off_t count)
{
    long __res;
    __asm__ volatile ("int $0x80"
                      : "=a" (__res)
                      : "0" (__NR_##write),"b" ((long)(fd)),"c" ((long)(buf)),"d" ((long)(count))); 
    if (__res>=0)
        return (int) __res;
    errno=-__res;
    return -1;
}

注意:这里在开头预先定义了一个宏,__LIBRARY__使得_syscall3和__NR_write有定义。

三、128号系统中断

3.1 system_call函数

    在kernel/sched.c(p103)的第411行,绑定了系统调用的处理函数:

set_system_gate(0x80,&system_call); 

其中,system_call位于kernel/system_call.s(p86)的第80行:

system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
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
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret

    该函数首先检查调用号是否超出界限,然后保存参数到堆栈中,将数据段和扩展段改为内核数据段,将FS设置为用户数据段。然后调用系统调用表中的对应的函数,并将返回结果保存到堆栈中。在返回时首先检查当前进程是否处于可执行状态且时间片未用完,否则将切换,回来时将继续往下执行。最后返回时如果发现系统调用发生时处于用户态,则检查当前进程的信号位图,并对信号进行处理。然后返回用户态,可能要先执行信号句柄。

3.2 sys_函数执行前的堆栈

    首先,在用户态下使用int 0x80,将切换到内核态,使用进程内核态的堆栈,该堆栈位于进程控制块的末端,最多是一页内存。开始执行system_call前的堆栈的内容为:

 

 

此时SS = 0x10, SP = current + PAGE_SIZE - 20

在调用sys_函数前,堆栈变为:

  

然后DS、ES指向内核数据段,FS指向用户数据段。此时SP = current + PAGE_SIZE - 44。并调用相应的sys_函数:

call sys_call_table(,%eax,4)

其中eax保存的是调用号,第三个参数4表示eax * 4,值为sys_call_table + eax * 4。如果没有则默认第三个参数是1。也就是说,上述场景将是sys_函数执行时的上下文,所有sys_函数都认为FS是用户数据段,而DS,ES都是内核数据段。

3.3 sys_fork函数的执行

sys_fork的定义在kernel/system_call.s的第208行(p89):

.align 2
sys_fork:
call find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process
addl $20,%esp
1: ret

它首先调用find_empty_process,位于kernel/fork.c的第135行(p115):

int find_empty_process(void)
{
    int i;

repeat:
    if ((++last_pid)<0) last_pid=1;
    for(i=0 ; i<NR_TASKS ; i++)
        if (task[i] && task[i]->pid == last_pid) goto repeat;
    for(i=1 ; i<NR_TASKS ; i++)
        if (!task[i])
            return i;
    return -EAGAIN;
}

该函数主要首先获取还没有使用的进程ID,并保存到last_pid中,该last_pid就是独一无二的,没被当前系统任何进程使用。然后再找一个进程号,也就是没被使用的进程下标,然后返回进程号。该进程号保存在eax中,如果符号位为1,返回值是负数,则直接返回出错。否则将调用copy_process,该函数开始执行时,堆栈映像如下:

 

 

copy_process函数位于kernel/fork.c第68行(p114):

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)
{
    struct task_struct *p;
    int i;
    struct file *f;

    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
    task[nr] = p;
    __asm__ volatile ("cld");
    *p = *current; /* NOTE! this doesn't copy the supervisor stack */
    p->state = TASK_UNINTERRUPTIBLE;
    p->pid = last_pid;
    p->father = current->pid;
    p->counter = p->priority;
    p->signal = 0;
    p->alarm = 0;
    p->leader = 0; /* process leadership doesn't inherit */
    p->utime = p->stime = 0;
    p->cutime = p->cstime = 0;
    p->start_time = jiffies;
    p->tss.back_link = 0;
    p->tss.esp0 = PAGE_SIZE + (long) p;
    p->tss.ss0 = 0x10;
    p->tss.eip = eip;
    p->tss.eflags = eflags;
    p->tss.eax = 0;
    p->tss.ecx = ecx;
    p->tss.edx = edx;
    p->tss.ebx = ebx;
    p->tss.esp = esp;
    p->tss.ebp = ebp;
    p->tss.esi = esi;
    p->tss.edi = edi;
    p->tss.es = es & 0xffff;
    p->tss.cs = cs & 0xffff;
    p->tss.ss = ss & 0xffff;
    p->tss.ds = ds & 0xffff;
    p->tss.fs = fs & 0xffff;
    p->tss.gs = gs & 0xffff;
    p->tss.ldt = _LDT(nr);
    p->tss.trace_bitmap = 0x80000000;
    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    if (copy_mem(nr,p))
    {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
    for (i=0; i<NR_OPEN; i++)
        if ((f=p->filp[i]))
            f->f_count++;
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
    p->state = TASK_RUNNING; /* do this last, just in case */
    return last_pid;
}

该函数首先使用get_free_page获得一页未使用的内存,作为任务控制块使用,由于是在内核空间中,且基地址为0,所以得到的地址即为物理地址。然后拷贝当前进程的任务控制块作为副本,并将新建的任务的状态设置为不可中断,这样方便对控制块和任务状态段进行修改。

新任务的进程ID修改为last_pid。

父进程ID修改为current->pid。

时间片修改为15个滴答。

信号位图、警报时间、领导权、用户态和内核态运行时间、子进程用户态和内核态运行时间均设置为0。

然后对TSS中的相关字段进行修改,将进程的内核态堆栈修改为任务控制块的末端,EIP指向为int 0x80的下一条地址,EAX设置为0,这是子进程fork之后返回0的原因。copy_process传递进来的参数除了nr和none以外都用来初始化新进程的TSS,这些参数都是父进程fork之前的状态,结束之后都将用于恢复现场。由于当前进程并未运行,且加载时所有寄存器的值都将来自TSS,当子进程被调度时,将直接运行int 0x80的下一条指令,处于用户态下的库函数里,堆栈为到int 0x80这条指令为止的用户态下的堆栈,所以要求int 0x80之前尽量不要有函数调用。得到的返回值为0。内核提供的库函数fork中,使用inline的形式,来避免函数调用,保证用户态堆栈的干净。

init/main.c第23行(p63)中,有

inline _syscall0(int,fork)  __attribute__((always_inline));

对于0进程fork出进程1,需要进程0的用户态堆栈足够干净,也就是user_stack足够干净。

新建立的进程将利用任务号建立对应的代码段和数据段基地址,并设置LDT中的基地址,LDT选择子重新设置。复制页表到新的页表,也就是使用相同的页,使用的是写时复制。同时,打开的文件指针、可执行文件、进程的根目录、当前根目录的引用计数将增加,最后设置在GDT中设置TSS和LDT的地址,设置新进程为可运行状态,返回子进程的ID。这些工作都是由父进程完成的,子进程一直未被调度。

新进程代码段和数据段的基地址为NR * 64MB,GDT中的表项为gdt + NR * 2 + FIRST_TSS_ENTRY, gdt + NR * 2 + FIRST_LDT_ENTRY,控制块为task[NR]。

3.4 sys_execve函数的执行

sys_execve定义在kernel/system_call.s的第200行(p88):

sys_execve:
lea EIP(%esp),%eax
pushl %eax
call do_execve
addl $4,%esp
ret

对于sys_execve而言,其参数已经保存了(当前堆栈上的ebx,ecx,edx),该函数调用的实际函数是do_execve,其看到的堆栈为

 

其中do_execve函数位于fs/exec.c第182行(p318),在第344和345行,对堆栈中的EIP和ESP进行了修改:

eip[0] = ex.a_entry; /* eip, magic happens :-) */

eip[3] = p; /* stack pointer */

也就是说,之后系统调用之后不会执行int 0x80下面那条语句,而是执行刚加载的代码,而且参数已经在栈中。这时main函数的形式参数(argc, argv)已经存在,直接跳到main函数执行。

int do_execve(unsigned long * eip,long tmp,char * filename, char ** argv, char ** envp)
{
    struct m_inode * inode;
    struct buffer_head * bh;
    struct exec ex;
    unsigned long page[MAX_ARG_PAGES];
    int i,argc,envc;
    int e_uid, e_gid;
    int retval;
    int sh_bang = 0;
    unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;

    if ((0xffff & eip[1]) != 0x000f)
        panic("execve called from supervisor mode");
    for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */
        page[i]=0;
    if (!(inode=namei(filename))) /* get executables inode */
        return -ENOENT;
    argc = count(argv);
    envc = count(envp);
restart_interp:
    if (!S_ISREG(inode->i_mode))   /* must be regular file */
    {
        retval = -EACCES;
        goto exec_error2;
    }
    i = inode->i_mode;
    e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
    e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
    if (current->euid == inode->i_uid)
        i >>= 6;
    else if (current->egid == inode->i_gid)
        i >>= 3;
    if (!(i & 1) &&
            !((inode->i_mode & 0111) && suser()))
    {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    if (!(bh = bread(inode->i_dev,inode->i_zone[0])))
    {
        retval = -EACCES;
        goto exec_error2;
    }
    ex = *((struct exec *) bh->b_data); /* read exec-header */
    if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang))
    {
        /*
         * This section does the #! interpretation.
         * Sorta complicated, but hopefully it will work.  -TYT
         */

        char buf[1023], *cp, *interp, *i_name, *i_arg;
        unsigned long old_fs;

        strncpy(buf, bh->b_data+2, 1022);
        brelse(bh);
        iput(inode);
        buf[1022] = '\0';
        if ((cp = strchr(buf, '\n')))
        {
            *cp = '\0';
            for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
        }
        if (!cp || *cp == '\0')
        {
            retval = -ENOEXEC; /* No interpreter name found */
            goto exec_error1;
        }
        interp = i_name = cp;
        i_arg = 0;
        for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
        {
            if (*cp == '/')
                i_name = cp+1;
        }
        if (*cp)
        {
            *cp++ = '\0';
            i_arg = cp;
        }
        /*
         * OK, we've parsed out the interpreter name and
         * (optional) argument.
         */
        if (sh_bang++ == 0)
        {
            p = copy_strings(envc, envp, page, p, 0);
            p = copy_strings(--argc, argv+1, page, p, 0);
        }
        /*
         * Splice in (1) the interpreter's name for argv[0]
         *           (2) (optional) argument to interpreter
         *           (3) filename of shell script
         *
         * This is done in reverse order, because of how the
         * user environment and arguments are stored.
         */
        p = copy_strings(1, &filename, page, p, 1);
        argc++;
        if (i_arg)
        {
            p = copy_strings(1, &i_arg, page, p, 2);
            argc++;
        }
        p = copy_strings(1, &i_name, page, p, 2);
        argc++;
        if (!p)
        {
            retval = -ENOMEM;
            goto exec_error1;
        }
        /*
         * OK, now restart the process with the interpreter's inode.
         */
        old_fs = get_fs();
        set_fs(get_ds());
        if (!(inode=namei(interp)))   /* get executables inode */
        {
            set_fs(old_fs);
            retval = -ENOENT;
            goto exec_error1;
        }
        set_fs(old_fs);
        goto restart_interp;
    }
    brelse(bh);
    if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
            ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||
            inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex))
    {
        retval = -ENOEXEC;
        goto exec_error2;
    }
    if (N_TXTOFF(ex) != BLOCK_SIZE)
    {
        printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
        retval = -ENOEXEC;
        goto exec_error2;
    }
    if (!sh_bang)
    {
        p = copy_strings(envc,envp,page,p,0);
        p = copy_strings(argc,argv,page,p,0);
        if (!p)
        {
            retval = -ENOMEM;
            goto exec_error2;
        }
    }
    /* OK, This is the point of no return */
    if (current->executable)
        iput(current->executable);
    current->executable = inode;
    for (i=0 ; i<32 ; i++)
        current->sigaction[i].sa_handler = NULL;
    for (i=0 ; i<NR_OPEN ; i++)
        if ((current->close_on_exec>>i)&1)
            sys_close(i);
    current->close_on_exec = 0;
    free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
    if (last_task_used_math == current)
        last_task_used_math = NULL;
    current->used_math = 0;
    p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
    p = (unsigned long) create_tables((char *)p,argc,envc);
    current->brk = ex.a_bss +
                   (current->end_data = ex.a_data +
                                        (current->end_code = ex.a_text));
    current->start_stack = p & 0xfffff000;
    current->euid = e_uid;
    current->egid = e_gid;
    i = ex.a_text+ex.a_data;
    while (i&0xfff)
        put_fs_byte(0,(char *) (i++));
    eip[0] = ex.a_entry; /* eip, magic happens :-) */
    eip[3] = p; /* stack pointer */
    return 0;
exec_error2:
    iput(inode);
exec_error1:
    for (i=0 ; i<MAX_ARG_PAGES ; i++)
        free_page(page[i]);
    return(retval);
}

这个函数首先计算参数argv、envp字符串的个数,这两个变量都是字符串指针数组,以NULL结尾。然后根据当前进程的有效用户ID是否为这个可执行文件的属主,有效用户组ID是否为这个可执行文件的属组,或者是其他成员,来确定是否有执行权限。如果当前用户没有执行权限,但却是超级用户,且三种情况中至少有一个执行权限,则可以执行。否则不可以执行。然后读取可执行文件的第一个数据块(1kB),判断是否为脚本文件。如果不是脚本文件则得到可执行文件头,对可执行文件头判断是否合法。

如果是脚本文件(前两个字符#!),则将envp里面的数组的最后一项从32页物理内存的末端(最后的4个字节不用)开始复制字符串(包含0结尾),字符串也是从后往前开始复制,然后将第一个参数去掉,将参数也类似复制,再将脚本文件名复制,并将传递给解释文件(如/bin/bash)的参数串复制(如果有,脚本文件的第一行),将解释文件的文件名复制(不是路径,如bash)。最后读取解释文件,并读取其解释文件(/bin/bash)的可执行文件头,不用再传递参数,开始执行解释文件(/bin/bash)。

如果是一般的可执行文件,则也复制参数,但不去掉第一个参数。

然后将当前进程的旧的可执行节点释放,并指向新的可执行节点,去掉所有从父进程继承过来的信号句柄,并置为默认。然后将部分从父进程继承过来的打开的文件句柄关闭,即将当前close_on_exec中置位的文件句柄关闭。然后close_on_exec清零。接着释放旧的内存页和页表,将LDT中代码段的段长设置为可执行文件中的代码段段长(向上取整,以PAGE_SIZE为基准),将数据段段长设置为64MB。并将设置的参数物理页(最多32页)映射到进程地址空间的末端(64MB * (NR + 1) - 4KB开始,往低地址处走一页一页映射),作为进程的堆栈。接着将每个环境变量的地址继续放到堆栈中,并以NULL指针结束。将参数变量也这么做。最后放置argc,argv,envp的值,表示字符串指针数组的起始位置。这样得到的堆栈指针即为开始时的指针。再设置进程的末尾brk = ex.a_text + ex.a_data + ex.a_bss,初始化最多一页内存,且值为0。重新设置代码入口地址和栈顶。

 

count函数的工作是计算这个二维指针数组有多少个字符串,计算的依据是以NULL结尾为标志数组结束的。char* p[] = {“HOME=/”, “PATH=/bin”, “CLASSPATH=.”, “hello”, NULL},上述共有4个字符串。

copy_strings这个函数的工作便是把这些字符串(包含0)整理到一维数组中,且一般是从用户空间到内核空间:

 


create_tables函数的作用是把字符串的起始地址放到字符串所在的数组中,并加入NULL,最后再把字符串指针数组的首地址以及个数放入数组中:

 

注意:上述两幅图中的PATH和HOME位置必须交换才是正确的。

而对于do_execve()来说,其主要的工作在于布置参数,其堆栈最终结果如下,这也是(启动/bin/bash进程进行解释)或者普通可执行文件开始执行时看到的用户态堆栈,注意堆栈向低地址生长:

  

 

由此,我们可以得到进程的地址空间为:

 

在do_execve函数中,还出现了一些字符串库函数的使用,如strncpy和strchr函数,这两个函数均位于include/string.h(p364)中,strncpy在第38行,strchr在第128行,这两个函数要求DS和ES指向相同的数据空间,如同时是内核空间,或者同时是用户态空间。

static inline char * strncpy(char * dest,const char *src,int count)
{
    __asm__("cld\n"
            "1:\tdecl %2\n\t"
            "js 2f\n\t"
            "lodsb\n\t"
            "stosb\n\t"
            "testb %%al,%%al\n\t"
            "jne 1b\n\t"
            "rep\n\t"
            "stosb\n"
            "2:"
            ::"S" (src),"D" (dest),"c" (count));
    return dest;
}
static inline char * strchr(const char * s,char c)
{
    register char * __res ;
    __asm__("cld\n\t"
            "movb %%al,%%ah\n"
            "1:\tlodsb\n\t"
            "cmpb %%ah,%%al\n\t"
            "je 2f\n\t"
            "testb %%al,%%al\n\t"
            "jne 1b\n\t"
            "movl $1,%1\n"
            "2:\tmovl %1,%0\n\t"
            "decl %0"
            :"=a" (__res):"S" (s),"0" (c));
    return __res;
}

注意这两个函数中地址与寄存器的对应关系,说明地址其实是虚拟地址(还需要段寄存器或者段描述符来指定所在的段),而且是标号,是一个符号。

3.5 do_signal函数的执行

        sys_函数执行后,system_call将会把返回值(eax)保存到栈中,如果当前进程时间片结束,或者处于非可运行状态,则切换进程。但不管怎样,都会执行ret_from_sys_call标号处的结束代码(被切换进程再度执行时也会执行)。如果发生系统调用时处于内核态,则直接结束。否则检查当前进程的信号位图,去掉被阻塞的部分,从低位开始检查是否有置位,有则复位,并调用do_signal函数。最后使用iret结束。

对于do_signal,其看到的内核态堆栈内容为:


 

do_signal执行完后,把信号值弹出,并把相应的寄存器弹出,上图加粗的部分。do_signal函数位于kernel/signal.c的第82行(p106)的函数原型:

void do_signal(long signr,long eax, long ebx, long ecx, long edx,
               long fs, long es, long ds,
               long eip, long cs, long eflags,
               unsigned long * esp, long ss)
{
    unsigned long sa_handler;
    long old_eip=eip;
    struct sigaction * sa = current->sigaction + signr - 1;
    int longs;
    unsigned long * tmp_esp;

    sa_handler = (unsigned long) sa->sa_handler;
    if (sa_handler==1)
        return;
    if (!sa_handler)
    {
        if (signr==SIGCHLD)
            return;
        else
            do_exit(1<<(signr-1));
    }
    if (sa->sa_flags & SA_ONESHOT)
        sa->sa_handler = NULL;
    *(&eip) = sa_handler;
    longs = (sa->sa_flags & SA_NOMASK)?7:8;
    *(&esp) -= longs;
    verify_area(esp,longs*4);
    tmp_esp=esp;
    put_fs_long((long) sa->sa_restorer,tmp_esp++);
    put_fs_long(signr,tmp_esp++);
    if (!(sa->sa_flags & SA_NOMASK))
        put_fs_long(current->blocked,tmp_esp++);
    put_fs_long(eax,tmp_esp++);
    put_fs_long(ecx,tmp_esp++);
    put_fs_long(edx,tmp_esp++);
    put_fs_long(eflags,tmp_esp++);
    put_fs_long(old_eip,tmp_esp++);
    current->blocked |= sa->sa_mask;
}

由于C语言约定参数从后往前入栈,由调用者清理堆栈,可以看出这些参数都已经为do_signal的执行准备好。在include/signal.h的第45行(p361)中,定义了信号默认处理句柄和忽视句柄:

#define SIG_DFL ((void (*)(int))0) /* default signal handling */

#define SIG_IGN ((void (*)(int))1) /* ignore signal */

这里首先是判断当前信号对应的函数句柄是否为1,若是则忽视,不做任何事情。然后判断是否为默认句柄,若是则除了SIGCHLD忽视,其他信号均终止进程的执行。

sigaction定义在include/signal.h的第48行(p361):

struct sigaction
{
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

其中sa_restorer用于清理用户态堆栈参数。

开始时,任务0的信号句柄均为0,表示默认处理。当前信号可以捕获时,如果在sa->sa_flags中设置了SA_ONESHOT则该句柄执行一次之后,就会恢复为默认值(通过获取该句柄的地址,对其值设置为0)。这里主要是修改int 0x80后执行的下一条指令为函数句柄,在用户态堆栈下添加参数(使用put_fs_long在内核态下向用户空间写数据),并修改用户态下的堆栈指针,为信号句柄的执行提供环境。并将当前句柄的屏蔽码添加到current->blocked中,这样当信号再次被调用时相关信号将被阻塞,不做处理。这样可以避免重入,因为信号是异步发生的。

假设没有sa->sa_flags为0,也就是SA_NOMASK没有置位,禁止信号重入。这时为信号句柄设置的用户态堆栈将占用8 x 4个字节,而且所有变量都是从内核态栈复制到用户态栈:

 

注意:上图old_eip <=> int 0x80下一条指令的地址,且old_esp 指在old_eip的上一个变量。

内核态堆栈映像:

 

所以系统调用返回后,将在用户态下首先执行该信号的函数句柄,然后再继续执行系统调用之后的指令,开始正常运行。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux 0.11 系统调用的实现机制 的相关文章

  • 学习笔记:SLAM_因子图优化

    SLAM 因子图优化 Factor graoh 模型 概率模型到因子图模型 定义一个简单的机器人问题 xff08 SLAM xff09 假设有一个机器人在往前运动 xff0c 运动过程中能观测到两个路标点 xff0c 定义了三个时间 xff
  • [NVIDIA]-2 从零入手 Jetson Xavier NX 内核编译、源码编译流程

    NVIDIA 从零入手 Jetson Xavier NX 内核编译 源码编译流程 注明 xff1a 感谢网友提醒 xff0c 我当时买的时候nx套件还没开售 xff0c 因为核心板兼容b01 xff0c 先买了核心板和b01的板子合在一起用
  • [NVIDIA]-1 入手Jetson Xavier NX 刷机+开机配置+系统烧录+Bring up

    NVIDIA 入手 Jetson Xavier NX 刷机 43 开机配置 43 系统烧录 43 Bring up 注明 xff1a 感谢网友提醒 xff0c 我当时买的时候nx套件还没开售 xff0c 因为核心板兼容b01 xff0c 先
  • RTOS解读一

    什么是RTOS 维基百科定义 实时操作系统 Real Time Operating System RTOS xff0c 通常读作 34 R toss 34 xff0c 指的是专为实时应用而设计的多任务操作系统 其应用场合包括嵌入式系统 可编
  • switch-case使用方法

    switch case 语句判断一个变量与一系列值中某个值是否相等 xff0c 每个值称为一个分支 switch case 语句语法格式如下 xff1a switch语句应用举例1 xff1a public class VariableTe
  • Linux单片机串口通信总结

    这是一个目录 Linux与单片机串口通信运行ROS串口发送节点后异常中断栈溢出问题catkin make报错 xff1a 函数未定义的引用ERROR L107 ADDRESS SPACE OVERFLOW串口实验总结程序组织串口调试 Lin
  • Apache Options指令详解

    前言 xff1a Options指令是Apache配置文件中一个比较常见也比较重要的指令 xff0c Options指令可以在Apache服务器核心配置 server config 虚拟主机配置 virtual host 特定目录配置 di
  • 欠驱动机械臂运动学仿真

    这是个目录 三轴机械臂建模及运动学仿真各仿真项目的个人理解三轴机械臂 xff08 欠驱动 xff09 分析难点更改RTB中逆解求解源码求解析解数值求解工作空间筛选 实用的解析解法总结 三轴机械臂建模及运动学仿真 在开始具体的机械结构及驱动结
  • postman安装使用教程(标贝科技)

    postman安装使用教程 文章目录 postman安装使用教程前言一 postman安装二 postman使用 前言 postman是Chrome浏览器的插件 xff0c 是一款功能强大的网页调试工具 xff08 接口调试神器 xff09
  • 流媒体推流原理

    我们知道一个完整的直播过程 xff0c 包括采集 处理 编码 封包 推流 传输 转码 分发 解码 播放等 xff0c 这一过程所采用的技术 xff0c 我们也称之为 流媒体技术 其中推流是指使用推流工具等内容抓取软件把直播内容传输到服务器的
  • 机器学习必知必会10大算法!

    Datawhale干货 作者 xff1a Fahim ul Haq xff0c 编译 xff1a InfoQ 现在 xff0c 机器学习有很多算法 如此多的算法 xff0c 可能对于初学者来说 xff0c 是相当不堪重负的 今天 xff0c
  • C语言结构体及链表定义

    最近在看 大话数据结构 xff0c 一边看书一边跑一下书中的案例 xff0c 加深下理解 书中的案例都是C写的 xff0c 顺便熟悉下C语言 此处第三章线性表链式存储 xff1a 在用代码描述单链表之前 xff0c 我们需要定义一个结构体来
  • 【ROS教程 005】ROS可视化

    在ROS系统中它可以通过一些通用工具轻松绘制标量数据图 xff0c 它要求对每一个标量字段数据分别绘制成二维曲线 xff08 1 xff09 用rxplot画出时间趋势曲线 在ROS系统中 xff0c 标量数据可以根据消息中提供的时间戳作为
  • C/C++/Windows/VC/MFC/Unix/Linux编程书籍推荐

    C C 43 43 编程书籍 C Primer Plus C 43 43 Primer C 43 43 Primer Plus C和指针 C陷阱与缺陷 C专家编程 C 43 43 沉思录 C语言深度剖析 Effective C 43 43
  • 使用CPM管理CMake C++工程中的外部依赖库

    严正声明 xff1a 本文系作者davidhopper原创 xff0c 未经许可 xff0c 不得转载 众所周知 xff0c 对于外部依赖库的管理是CMake C 43 43 工程中一个令人头疼的问题 人们一直希望能有一个工具来自动配置CM
  • 字典和集合2:高效性和可哈希

    目录 1 字典和集合的高效性 2 散列表操作 2 1 散列表的写入 2 2 散列表的查找 2 3 散列表的缺点 3 可哈希 1 字典和集合的高效性 许多时候 xff0c 将列表改为字典或集合后 xff0c 执行效率将会有巨大的飞跃 xff0
  • 卡尔曼滤波(Kalman filter)公式推导详细版

    卡尔曼滤波 xff08 Kalman filter xff09 公式推导详细版 记得很早的时候 xff0c 我曾经手工推导过卡尔曼滤波 xff0c 但是之前的已经大多记不起来了 今天帮着老师整理PPT的时候 xff0c 老师让我补充完相应的
  • 4g dtu透传模块

    4g dtu透传模块 xff0c 是一款支持双向透明传输的产品 xff0c 用户使用无需关心复杂的协议 xff0c 产品为双向透传 xff0c 只需要简单配置即可 同时产品为4G全网通 xff0c 支持国内全部运营商网络 xff0c 通讯模
  • PCB设计学习笔记(一)原理图界面

    一 画原理图库 多个引脚可以ctrl 43 c一个引脚后 xff0c 编辑 gt 设置粘贴阵列 xff0c 一次性复制出来多个引脚 二 原理图界面 生成原理图库 xff1a 在一个现成的原理图界面可以直接将现有的原理图内的原件生成一个原理图
  • novatel计算odom--GPS坐标与UTM坐标转换

    保证你的novatel的driver是在ros drivers上的驱动 1 简介 1 1 消息 gps common定义了两个通用消息 xff0c 供GPS驱动程序输出 xff1a gps common GPSFix和gps common

随机推荐

  • 搭建自己的slam系统

    手头有的传感器 一个vlp16线激光雷达 一个120 的fisheye相机 一整套novatel GPS 43 IMU 采集了一个闭环的bag 第一步 数据预处理 将camera的数据单独提取出来 因为没有时间戳 因此无法做到传感器的紧耦合
  • KCF_ROS + TLD_ROS 原理以及源码分析

    扩展链接 xff1a 百度搜素vot2016 xff0c 会有相关的资料 KCF缺点是有边界效应 有一个改进的版本是 xff1a SRDCF 大牛的 CSDN博客 说明 xff1a http blog csdn net app 120620
  • 数据结构+算法=程序

    xff08 1 xff09 数据结构 43 算法 61 程序 每个学计算机的人都听过这个公式 这个公式是尼克劳斯沃斯在1976年出版 算法 43 数据结构 xff1a 程序 一书中提出 尼克劳斯沃斯还是Pascal编程语言的发明人 xff0
  • librealsense源码编译-- Failed to identify Internet connection, disabling BUILD_WITH_TM2

    问题概述 xff1a 由于Realsense t265源码编译需要在CMakeList txt中将BUILD WITH TM2设置为ON xff0c 但是由于国内网络原因 xff0c 无法通过网络连接检测 xff0c 因此我通过查找文件 x
  • 内外网映射环境配置

    一 环境搭建 1 环境说明 此环境由内 外网组成 xff0c 主要实现内外网映射功能 2 环境配置图 本手册中 xff0c 内网使用78 网段 xff0c 外网使用 20 网段 外网交换机IP 地址为 192 168 20 1 3 配置路由
  • 浅析extern “C”的作用

    浅析extern C 的作用 关于extern C 的作用和意思 xff0c 网上资料已经有很多了 xff08 我也参考了几篇 xff09 xff0c 不过我还是觉得有必要自己总结一下 xff0c 毕竟 好记性不如烂笔头 嘛 到C标准函数库
  • 使用prometheus+grafana监控k8s集群

    Prometheus官网地址 xff1a https prometheus io GitHub地址 xff1a https github com prometheus prometheus 一 prometheus的安装 Prometheu
  • ARM 学习笔记(四) 快速上下文切换(FCSE)技术

    接上回说 xff0c 我们已经了解存储器管理的方法 xff0c 现在我们来看一下 xff0c ARM 对不同进程的地址管理 快速上下文切换技术 xff08 Fast Context Switch Extension FCSE xff09 F
  • Linux与Windows串口通信

    串口是常用的计算机与外部串行设备之间的数据传输通道 xff0c 由于串行通信方便易行 xff0c 所以应用广泛 现在国际上不断有串口新技术及新规格推出 xff0c 结合社会各方面需要 xff0c 串口通信发展的空间庞大 串口通讯技术因其自身
  • Git代码下载以及子模块更新git submodule update --init

    Git代码下载 加速代码下载 xff1a git clone https ghproxy com https github com 加入https ghproxy com 后下载速度会快很多 xff0c 还是由于科学上网的问题 子模块下载
  • 解决UnicodeEncodeError: 'gbk' codec can't encode character '\xbb' in position

    在抓取网页上面的一些源代码的时候 xff0c 将uft 8的编码写入文档 xff0c 并输出的时候 xff0c 出现这了这个报错 xff0c 说gbk无法编码 xbb 查找相关资料 xff1a 1 str转bytes叫encode xff0
  • FIFO(First-In First-Out)先进先出页面置换算法详解

    FIFO xff08 First In First Out xff09 先进先出页面置换算法 xff1a FIFO淘汰算法总是淘汰最先装入内存的页面 xff0c 即选择在内存中驻留时间最久的页面进行淘汰 该算法实现只需把一个进程已调入内存的
  • 解决方法:git遇到过的错误

    用git也是走了很多弯路 比如 xff1a 这种错误 1 git commit a fatal Unable to create 39 E git index lock 39 File exists Another git process
  • 路径规划学习入门

    运动规划简介 当虚拟人开始一次漫游时 xff0c 首先全局规划器根据已有的长期信息进行全局静态规划 xff0c 确定虚拟人应该经过的最优化路线 然后全局规划器控制执行系统按照该路径运动 在运动过程中 xff0c 感知系统会持续对周围环境进行
  • 测试开发工程师面试题目

    测试开发工程师面试题目 1 什么是兼容性测试 兼容性测试侧重哪些方面 主要检验的是软件的可移植性 xff0c 检查软件在不同的硬件平台软件平台上是否可以正常的运行 细分会有 xff1a 平台的兼容 xff0c 网络兼容 xff0c 数据库兼
  • 【ROS2】四、使用tf坐标变换实现小海龟跟随

    1 创建功能包 创建工作空间并在工作空间下创建功能包 xff1a span class token function mkdir span p turtle tf ws src span class token builtin class
  • 滑模控制简单理解(hm-1)

    变结构控制 xff08 VSC xff09 是一种特殊的非线性控制器 xff0c 表现为控制的不连续性 xff0c 又称滑模控制 xff08 SMC xff09 一般步骤为滑模面的设计 趋近率的设计 控制器的求解 滑模控制的理解 如图所示
  • BP神经网络的数学本质

    原创文章 xff0c 转载请说明来自 老饼讲解 BP神经网络 xff1a bp bbbdata com 目录 一 隐神经元与tansig函数 二 BP本质 三 BP神经网络的曲线拟合要素 一 误差函数 二 拟合基函数 三 待解参数与求解算法
  • GAZEBO_PLUGIN_PATH 设置方法

    http answers gazebosim org question 13391 how to set gazebo plugin path correctly and add the plugin into gazebo ros Hi
  • Linux 0.11 系统调用的实现机制

    Linux 0 11 系统调用的实现机制 一 系统调用概述 系统调用本质上是一种中断 xff0c 中断号为0x80 xff0c 即128号中断 通常我们使用的是库函数 xff0c 而不是直接使用系统调用 xff0c 这主要是因为库函数一般都