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
顯然,exit是1號,fork是2號,read是3號,write是4號。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下直接編譯,最佳答