1. 程式人生 > >Linux 0.11 系統呼叫的實現機制

Linux 0.11 系統呼叫的實現機制

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的上一個變數。

核心態堆疊映像:

 

所以系統呼叫返回後,將在使用者態下首先執行該訊號的函式控制代碼,然後再繼續執行系統呼叫之後的指令,開始正常執行。

相關推薦

Linux 0.11 系統呼叫實現機制

Linux 0.11 系統呼叫的實現機制 一、系統呼叫概述     系統呼叫本質上是一種中斷,中斷號為0x80,即128號中斷。通常我們使用的是庫函式,而不是直接使用系統呼叫,這主要是因為庫函式一般都是規定好的,是可以移植的。而系統呼叫的具體子呼叫號可能會發生改變,不同平臺

Linux 系統呼叫實現機制

1. 提示:unable to copy the source file ./installer/services.sh to the destination file /etc/init.d/vmware-tools     錯誤原因: 我的解壓包的目錄是 /mnt/cd

Linux fsync和fdatasync系統呼叫實現分析(Ext4檔案系統

參考:https://blog.csdn.net/luckyapple1028/article/details/61413724 在Linux系統中,對檔案系統上檔案的讀寫一般是通過頁快取(page cache)進行的(DirectIO除外),這樣設計的可以延時磁碟IO的操作,從而可以減少磁碟讀

Linux系統呼叫實現檔案操作

系統呼叫(系統呼叫是作業系統提供給使用者程式的一組“特殊”函式介面,使用者通過這組介面獲得作業系統提供的服務)中操作I/O的函式,都是針對檔案描述符的。 通過檔案描述符可以直接對相應檔案進行操作,如:open、close、write、read、ioctl #define STDIN_FIL

系統呼叫實現Linux命令 ls -al

二話不說直接上程式碼(這是我之前在網易部落格上寫的搬過來) ls.c 如下: #include "ls.h" /**********************************************************************/ //將路徑定位到

建立Linux 0.11完整的系統,讓linux 0.11真正轉起來!方便大家學習。 中文版權所有: OldLinux論壇 

為了配合Linux 0.11核心工作原理的學習,本章介紹了利用PC機模擬軟體和在實際計算機上執行Linux 0.11系統的方法。其中包括核心的編譯過程、PC模擬環境下檔案的訪問和複製、引導盤和根檔案系統的製作方法以及Linux 0.11系統的使用方法。最後還說明了如何對核心程

Linux VFS中write系統呼叫實現原理

目錄 WORD裡面的目錄複製過來似乎不能直接用。。還是放在這裡當主線看吧.. 使用者空間的write函式在核心裡面的服務例程為sys_write [email protected]

linux下增加一個新的系統呼叫實現pstree功能

這是我們linux課程的一個作業。 首先得到init程序的task_struct,根據list_for_each可以迴圈遍歷可以的到其所有的子程序的 list_head,根據list_head使用li

再探Linux核心write系統呼叫操作的原子性

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Linux核心完全註釋 閱讀筆記:3.5、Linux 0.11目標檔案格式

為了生成核心程式碼檔案,Linux 0.11使用了兩種編譯器。第一種是彙編編譯器as86和相應的連結程式(或稱為連結器)ld86。它們專門用於編譯和連結,執行在實地址模式下的16位核心引導扇區程式bootsect.s和設定程式setup.s。第二種是GNU的彙編器as

套介面層之socket系統呼叫實現

這篇筆記記錄了AF_INET協議族在套介面層對scoket()系統呼叫的實現,注意這裡只介紹了套介面層的實現,相比於完整的socket()系統呼叫實現,這裡缺少兩部分內容: 檔案系統相關的部分,比如檔案描述符的分配等; 傳輸層的實現,套接字的建立肯定是要傳輸層

Linux:訊號的底層實現機制

1.訊號:系統先定義好的某些特定的事件,可以被髮生,也可以被接受。發生和接受的主體都是程序。 2.訊號機制:系統預先定義好的一些事件 3.程序對訊號的響應方式:當程序發生時,使用者可以要求程序以以下三種方式之一對訊號做出響應:                a.預

系統技術非業餘研究 » Linux下新系統呼叫sync_file_range

我們在做資料庫程式或者IO密集型的程式的時候,通常在更新的時候,比如說資料庫程式,希望更新有一定的安全性,我們會在更新操作結束的時候呼叫fsync或者fdatasync來flush資料到持久裝置去。而且通常是以頁面為單位,16K一次或者4K一次。 安全性保證了,但是效能就有很大的損害。而且我們更新

ubuntu 12.04編譯及除錯linux-0.11

(更新中.....) 最近開始研究linux-0.11,編譯過程就遇到各種奇葩情況......好不容易編譯通過了....可是還是不能載入執行.... 下面是我學習過程記錄: 1.開始我用gcc-3.2.2-5.i386.rpm編譯linux-0.11, 你也知道rpm包在u

linux-0.11核心深度剖析視訊

大家好,歡迎觀看由BitEye工作室推出的《linux核心深度剖析》系列視訊。 本套視訊將以linux初學者的角度來探討linux0.11核心,之所以選擇linux0.11核心是基於以下幾個原因:

linux-0.11摳程式碼-GDB+VMWARE

vmware新建一個虛擬機器,硬碟為0.1G,建立完成後要先啟動一次虛擬機器,此時無任何系統,然後再關閉,應該會多出一個ostest-flat.vmdk這個虛擬磁碟檔案,下面要用到 新建完成後 我的虛擬機器叫OSTest,然後在虛擬機器根目錄下 有個OSTest.vmx配

Ubuntu 12.10安裝Bochs 2.6, 除錯linux-0.11核心

Linux(ubuntu)安裝bochs ubuntu下安裝bochs Ubuntu 10.04安裝Bochs 2.4.5筆記 bochs的安裝與使用 linux bochs的啟動 本文介紹在SUSE Linux Enterprise Des

利用bochs除錯linux 0.11核心

此時Bochs除錯系統已經準備好開始執行,CPU執行指標已指向ROM BIOS中地址0x000fffff0處的指令處。其中'<bochs:1>'是命令輸入提示符,其中的數字表示當前的命令序列號。在命令提示符'<bochs:1>'後面鍵入'help'命令,可以列出除錯系統的基本命令。

Linux Suse 11系統下的NFS配置

1.Server端1.1 檢查是否已經安裝NFS服務 檢查是否安裝nfs-kernel-server:vms240:~ # rpm -aq|grep nfs nfs-client-1.2.1-2.6.6 yast2-nfs-common-2.17.7-1.1.2nfs-kernel-server-1.2.1-

真正能在windows下編譯的linux 0.11,不是在Cygwin,也不是在虛擬機器裡!

一、簡介  這就是能在windows環境下直接編譯的Linux 0.11了,不是在Cygwin,也不是在虛擬機器裡,而是使用MinGW gcc,這是GNU gcc在Windows下的移植版本.在oldlinux上的論壇看見有許多人在問怎樣在Windows下直接編譯,最佳答