glibc源碼分析(一)系統調用

1.1 什麼是glibc

glibc是GNU發布的libc庫,即c運行庫。glibc是linux系統中最底層的api,幾乎其它任何運行庫都會依賴於glibc。glibc除了封裝linux操作系統所提供的系統服務外,它本身也提供了許多其它一些必要功能服務的實現。由於 glibc 囊括了幾乎所有的 UNIX 通行的標準,可以想見其內容包羅萬象。而就像其他的 UNIX 系統一樣,其內含的檔案群分散於系統的樹狀目錄結構中,像一個支架一般撐起整個操作系統。在 GNU/Linux 系統中,其C函式庫發展史點出了GNU/Linux 演進的幾個重要里程碑,用 glibc 作為系統的C函式庫,是GNU/Linux演進的一個重要里程碑。

glibc支持不同的體系結構,不同的體系結構之上又支持不同的操作系統。

  • 支持的體系結構:alpha,arm,i386,ia64,powerpc等
  • 支持的操作系統:bsd,linux等

本文及以後的一系列文章將對glibc源碼進行一系列的分析,這些分析都是基於i386體系結構linux操作系統。glibc版本號為glibc-2.26。

1.2 什麼是系統調用

1.2.1 概要

顧名思義,系統調用(system call)是指操作系統提供給程序調用的介面。

操作系統的主要功能是為管理硬體資源和為應用程序開發人員提供良好的環境來使應用程序具有更好的兼容性,為了達到這個目的,內核提供一系列具備預定功能的多內核函數,通過一組稱為系統調用(system call)的介面呈現給用戶。系統調用把應用程序的請求傳給內核,調用相應的的內核函數完成所需的處理,將處理結果返回給應用程序。

作為開發人員,我們調用系統調用來實現系統功能。

有過linux下開發經驗的人一定對glibc中的open,read,write,close,stat,mkdir等函數有所了解。這些函數其實都是是系統調用,準確的講是系統調用的封裝函數。glibc將諸多系統調用都封裝成函數,使我們可以以函數的方式,方便的調用系統調用。本文及後續章節將詳細講解glibc對系統調用封裝的過程。

1.2.2 實例

#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>#include <stdio.h>int main(int argc,char **argv){ struct stat buf; stat("/initrd.img",&buf); printf("size = %ld
",buf.st_size); return 0;}

1.3 系統調用的封裝

系統調用的封裝按照固定的規則進行。寄存器EAX傳遞系統調用號。系統調用號用來確定系統調用。寄存器EBX,ECX,EDX,ESI,EDI,EBP依次傳遞系統調用參數。參數個數決定設置寄存器的個數。int0x80指令切入內核執行系統調用。系統調用執行完成後返回。寄存器EAX保存系統調用的返回值。

glibc使用了多種不同的方式封裝系統調用。但是,萬變不離其宗,它們的封裝過程一定是按照上面的規則進行的。

1.4 glibc封裝系統調用

glibc實現了許多系統調用的封裝。它們的封裝方式大致可以分為兩種:一 腳本生成彙編文件,彙編文件中彙編代碼封裝了系統調用。這種方式,簡稱腳本封裝。二 .c文件中調用嵌入式彙編代碼封裝系統調用。一般使用.c文件封裝系統調用,代碼中除了嵌入式彙編封裝代碼外,還有一些C代碼做其他處理。這種方式,簡稱.c封裝。

1.5 腳本封裝

1.5.1 概要

glibc中大多數系統調用都是使用腳本封裝的方式封裝的。

腳本封裝的規則很簡單。三種文件生成封裝代碼。一 make-syscall.sh文件 二 syscall-template.S文件 三 syscalls.list文件。

make-syscall.sh是shell腳本文件。它讀取syscalls.list文件的內容,對文件的每一行進行解析。根據每一行的內容生成一個.S彙編文件,彙編文件封裝了一個系統調用。

syscall-template.S是系統調用封裝代碼的模板文件。生成的.S彙編文件都調用它。

syscalls.list是數據文件,它的內容如下:

# File name Caller Syscall name Args Strong name Weak namesaccept - accept Ci:iBN __libc_accept acceptaccess - access i:si __access accessacct - acct i:S acctadjtime - adjtime i:pp __adjtime adjtimebind - bind i:ipi __bind bindchdir - chdir i:s __chdir chdir......

它由許多行組成,每一行可分為6列。File name列指定生成的彙編文件的文件名。Caller指定調用者。Syscall name列指定系統調用的名稱,系統調用名稱可以轉換為系統調用號以標示系統調用。Args列指定系統調用參數類型,個數及返回值類型。Strong name指定系統調用封裝函數的函數名。Weak names列指定封裝函數的別稱,用戶可以調用別稱來調用封裝函數。

make-syscall.sh分析syscalls.list每一行每一列的內容,生成彙編文件。以分析chdir行為例,生成的彙編文件內容為:

#define SYSCALL_NAME chdir#define SYSCALL_NARGS 1#define SYSCALL_SYMBOL __chdir#define SYSCALL_CANCELLABLE 0#define SYSCALL_NOERRNO 0#define SYSCALL_ERRVAL 0#include <syscall-template.S>weak_alias (__chdir, chdir)hidden_weak (chdir)

SYSCALL_NAME宏定義了系統調用的名字。是從Syscall name列獲取。

SYSCALL_NARGS宏定義了系統調用參數的個數。是通過解析Args列獲取。

SYSCALL_SYMBOL宏定義了系統調用的函數名稱。是從Strong name列獲取。

SYSCALL_CANCELLABLE宏在生成的所有彙編文件中都定義為0。

SYSCALL_NOERRNO宏定義為1,則封裝代碼沒有出錯返回。用於getpid這些沒有出錯返回的系統調用。是通過解析Args列設置。

SYSCALL_ERRVAL宏定義為1,則封裝代碼直接返回錯誤號,不是返回-1並將錯誤號放入errno中。生成的所有.S文件中它都定義為0。

weak_alias (__chdir, chdir)定義了__chdir函數的別稱,我們可以調用chdir來調用__chdir。 chdir從Weak names列獲取。

彙編文件中引用了模板文件syscall-template.S,所有的封裝代碼都集中在syscall-template.S文件中。

3種文件,make-syscall.sh文件在sysdeps/unix/make-syscall.sh。syscall-template.S文件在sysdeps/unix/syscall-template.S。syscalls.list文件則有多個,分別在sysdeps/unix/syscalls.list,sysdeps/unix/sysv/linux/syscalls.list,sysdeps/unix/sysv/linux/generic/syscalls.list,sysdeps/unix/sysv/linux/i386/syscalls.list。

1.5.2 syscall-template.S

syscall-template.S作為模板文件,包含了所有封裝代碼。

#if SYSCALL_CANCELLABLE# include <sysdep-cancel.h>#else# include <sysdep.h>#endif#define syscall_hidden_def(SYMBOL) hidden_def (SYMBOL)#define T_PSEUDO(SYMBOL, NAME, N) PSEUDO (SYMBOL, NAME, N)#define T_PSEUDO_NOERRNO(SYMBOL, NAME, N) PSEUDO_NOERRNO (SYMBOL, NAME, N)#define T_PSEUDO_ERRVAL(SYMBOL, NAME, N) PSEUDO_ERRVAL (SYMBOL, NAME, N)#define T_PSEUDO_END(SYMBOL) PSEUDO_END (SYMBOL)#define T_PSEUDO_END_NOERRNO(SYMBOL) PSEUDO_END_NOERRNO (SYMBOL)#define T_PSEUDO_END_ERRVAL(SYMBOL) PSEUDO_END_ERRVAL (SYMBOL)#if SYSCALL_NOERRNOT_PSEUDO_NOERRNO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) ret_NOERRNOT_PSEUDO_END_NOERRNO (SYSCALL_SYMBOL)#elif SYSCALL_ERRVALT_PSEUDO_ERRVAL (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) ret_ERRVALT_PSEUDO_END_ERRVAL (SYSCALL_SYMBOL)#elseT_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) retT_PSEUDO_END (SYSCALL_SYMBOL)#endifsyscall_hidden_def (SYSCALL_SYMBOL)

文件開頭引入.h文件。如果SYSCALL_CANCELLABLE宏定義為1,則引入<sysdep-cancel.h>文件,否則引入<sysdep.h>文件。SYSCALL_CANCELLABLE宏在所有生成的彙編文件中都定義為0,所以彙編文件都是引用<sysdep.h>文件。<sysdep.h>文件位於sysdeps/unix/sysv/linux/i386/sysdep.h

#if SYSCALL_CANCELLABLE# include <sysdep-cancel.h>#else# include <sysdep.h>#endif

系統調用的封裝代碼由3種形式。

如果系統調用沒有錯誤返回,則執行

T_PSEUDO_NOERRNO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) ret_NOERRNOT_PSEUDO_END_NOERRNO (SYSCALL_SYMBOL)

如果系統調用有錯誤返回且直接返回錯誤,則執行

T_PSEUDO_ERRVAL (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) ret_ERRVALT_PSEUDO_END_ERRVAL (SYSCALL_SYMBOL)

如果系統調用有錯誤返回且返回-1,errno設置錯誤號,則執行

T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) retT_PSEUDO_END (SYSCALL_SYMBOL)

1.5.3 T_PSEUDO_NOERRNO

在系統調用沒有出錯返回時,執行

T_PSEUDO_NOERRNO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS) ret_NOERRNOT_PSEUDO_END_NOERRNO (SYSCALL_SYMBOL)

T_PSEUDO_NOERRNO宏引用PSEUDO_NOERRNO宏

T_PSEUDO_END_NOERRNO宏引用PSEUDO_END_NOERRNO宏

#define T_PSEUDO_NOERRNO(SYMBOL, NAME, N) PSEUDO_NOERRNO (SYMBOL, NAME, N)#define T_PSEUDO_END_NOERRNO(SYMBOL) PSEUDO_END_NOERRNO (SYMBOL)

#undef PSEUDO_NOERRNO#define PSEUDO_NOERRNO(name, syscall_name, args) .text; ENTRY (name) DO_CALL (syscall_name, args)

PSEUDO_NOERRNO宏在文件開頭聲明文件內容為代碼段

.text;

定義了名字為name的函數

#define ENTRY(name) .globl C_SYMBOL_NAME(name); .type C_SYMBOL_NAME(name),@function; .align ALIGNARG(4); C_LABEL(name) cfi_startproc; CALL_MCOUNT#ifndef C_SYMBOL_NAME# define C_SYMBOL_NAME(name) name#endif#define ALIGNARG(log2) 1<<log2 //代碼對齊# define C_LABEL(name) name##: //函數名# define cfi_startproc .cfi_startproc#define CALL_MCOUNT /* Do nothing. */

執行了系統調用

#undef DO_CALL#define DO_CALL(syscall_name, args) PUSHARGS_##args DOARGS_##args movl $SYS_ify (syscall_name), %eax; ENTER_KERNEL POPARGS_##args

DO_CALL宏根據命令行參數個數args的不同執行不同的宏。

當args為0時:

#define PUSHARGS_0 /* No arguments to push. */#define DOARGS_0 /* No arguments to frob. */#define POPARGS_0 /* No arguments to pop. */#define _PUSHARGS_0 /* No arguments to push. */#define _DOARGS_0(n) /* No arguments to frob. */#define _POPARGS_0 /* No arguments to pop. */

程序執行

movl $SYS_ify (syscall_name), %eax; ENTER_KERNEL //根據系統調用名,返回系統調用號#undef SYS_ify#define SYS_ify(syscall_name) __NR_##syscall_name//切入內核執行系統調用#ifdef I386_USE_SYSENTER# ifdef SHARED# define ENTER_KERNEL call *%gs:SYSINFO_OFFSET# else# define ENTER_KERNEL call *_dl_sysinfo# endif#else# define ENTER_KERNEL int $0x80#endif

當args為1時:

#define PUSHARGS_1 movl %ebx, %edx; L(SAVEBX1): PUSHARGS_0#define DOARGS_1 _DOARGS_1 (4)#define POPARGS_1 POPARGS_0; movl %edx, %ebx; L(RESTBX1):#define _PUSHARGS_1 pushl %ebx; cfi_adjust_cfa_offset (4); cfi_rel_offset (ebx, 0); L(PUSHBX1): _PUSHARGS_0#define _DOARGS_1(n) movl n(%esp), %ebx; _DOARGS_0(n-4)#define _POPARGS_1 _POPARGS_0; popl %ebx; cfi_adjust_cfa_offset (-4); cfi_restore (ebx); L(POPBX1):

程序執行:

movl %ebx, %edx;movl 4(%esp), %ebx;movl $SYS_ify (syscall_name), %eax; ENTER_KERNELmovl %edx, %ebx;

當args為2時:

#define PUSHARGS_2 PUSHARGS_1#define DOARGS_2 _DOARGS_2 (8)#define POPARGS_2 POPARGS_1#define _PUSHARGS_2 _PUSHARGS_1#define _DOARGS_2(n) movl n(%esp), %ecx; _DOARGS_1 (n-4)#define _POPARGS_2 _POPARGS_1

程序執行:

movl %ebx, %edx; movl 8(%esp), %ecx;movl 4(%esp), %ebx;movl $SYS_ify (syscall_name), %eax; ENTER_KERNELmovl %edx, %ebx;

當args為3時:

#define PUSHARGS_3 _PUSHARGS_2#define DOARGS_3 _DOARGS_3 (16)#define POPARGS_3 _POPARGS_3#define _PUSHARGS_3 _PUSHARGS_2#define _DOARGS_3(n) movl n(%esp), %edx; _DOARGS_2 (n-4)#define _POPARGS_3 _POPARGS_2

程序執行

pushl %ebx;movl 16(%esp), %edx;movl 12(%esp), %ecx;movl 8(%esp), %ebx;movl $SYS_ify (syscall_name), %eax;ENTER_KERNELpopl %ebx

當args參數為4時:

#define PUSHARGS_4 _PUSHARGS_4#define DOARGS_4 _DOARGS_4 (24)#define POPARGS_4 _POPARGS_4#define _PUSHARGS_4 pushl %esi; cfi_adjust_cfa_offset (4); cfi_rel_offset (esi, 0); L(PUSHSI1): _PUSHARGS_3#define _DOARGS_4(n) movl n(%esp), %esi; _DOARGS_3 (n-4)#define _POPARGS_4 _POPARGS_3; popl %esi; cfi_adjust_cfa_offset (-4); cfi_restore (esi); L(POPSI1):

程序執行:

pushl %esi;pushl %ebx;movl 24(%esp), %esi;movl 20(%esp), %edx;movl 16(%esp), %ecx;movl 12(%esp), %ebx;movl $SYS_ify (syscall_name), %eax;ENTER_KERNELpopl %ebx;popl %esi;

當參數為5時:

#define PUSHARGS_5 _PUSHARGS_5#define DOARGS_5 _DOARGS_5 (32)#define POPARGS_5 _POPARGS_5#define _PUSHARGS_5 pushl %edi; cfi_adjust_cfa_offset (4); cfi_rel_offset (edi, 0); L(PUSHDI1): _PUSHARGS_4#define _DOARGS_5(n) movl n(%esp), %edi; _DOARGS_4 (n-4)#define _POPARGS_5 _POPARGS_4; popl %edi; cfi_adjust_cfa_offset (-4); cfi_restore (edi); L(POPDI1):

程序執行

pushl %edi;pushl %esi;pushl %ebx;movl 32(%esp), %edi;movl 28(%esp), %esi;movl 24(%esp), %edx;movl 20(%esp), %ecx;movl 16(%esp), %ebx;movl $SYS_ify (syscall_name), %eax;ENTER_KERNELpopl %ebx;popl %esi;popl %edi;

當參數為6時:

#define PUSHARGS_6 _PUSHARGS_6#define DOARGS_6 _DOARGS_6 (40)#define POPARGS_6 _POPARGS_6#define _PUSHARGS_6 pushl %ebp; cfi_adjust_cfa_offset (4); cfi_rel_offset (ebp, 0); L(PUSHBP1): _PUSHARGS_5#define _DOARGS_6(n) movl n(%esp), %ebp; _DOARGS_5 (n-4)#define _POPARGS_6 _POPARGS_5; popl %ebp; cfi_adjust_cfa_offset (-4); cfi_restore (ebp); L(POPBP1):

程序執行

pushl %ebp; pushl %edi;pushl %esi;pushl %ebx;movl 40(%esp), %ebp;movl 36(%esp), %edi;movl 32(%esp), %esi;movl 28(%esp), %edx;movl 24(%esp), %ecx;movl 20(%esp), %ebx;movl $SYS_ify (syscall_name), %eax;ENTER_KERNELpopl %ebx;popl %esi;popl %edi;popl %ebp;

DO_CALL宏設置了系統調用參數,系統調用號,切入內核,並將系統調用返回值放入eax寄存器中。

接著,執行ret指令返回函數。

ret_NOERRNO#define ret_NOERRNO ret

彙編文件結尾

#undef PSEUDO_END_NOERRNO#define PSEUDO_END_NOERRNO(name) END (name)//彙編文件結束#undef END#define END(name) cfi_endproc; ASM_SIZE_DIRECTIVE(name)#define cfi_endproc .cfi_endproc#define ASM_SIZE_DIRECTIVE(name) .size name,.-name;

到這裡,整個封裝代碼已經全部完成。

1.5.4 T_PSEUDO_ERRVAL

#undef PSEUDO_ERRVAL#define PSEUDO_ERRVAL(name, syscall_name, args) .text; ENTRY (name) DO_CALL (syscall_name, args); negl %eax

T_PSEUDO_ERRVAL宏定義了函數name,函數調用了系統調用syscall_name。執行完DO_CALL 宏後,系統調用執行完畢,系統調用返回值放入eax寄存器中。negl %eax取反eax寄存器的值。此時,eax寄存器中保存著錯誤號。

#define ret_ERRVAL ret

函數返回

#undef PSEUDO_END_ERRVAL#define PSEUDO_END_ERRVAL(name) END (name)

彙編文件結尾

1.5.5 T_PSEUDO

#undef PSEUDO#define PSEUDO(name, syscall_name, args) .text; ENTRY (name) DO_CALL (syscall_name, args); cmpl $-4095, %eax; jae SYSCALL_ERROR_LABEL

執行系統調用,如果其返回值大於等於-4095,則跳到SYSCALL_ERROR_LABEL處執行。

#define SYSCALL_ERROR_LABEL __syscall_error

SYSCALL_ERROR_LABEL指向__syscall_error函數。

int__attribute__ ((__regparm__ (1)))__syscall_error (int error){ __set_errno (-error); return -1;}

如果小於-4095,則直接返回

ret

彙編文件結尾

#undef PSEUDO_END#define PSEUDO_END(name) SYSCALL_ERROR_HANDLER END (name)#define SYSCALL_ERROR_HANDLER /* Nothing here; code in sysdep.c is used. */

1.5.6 實例

chdir函數

umask函數

推薦閱讀:

不考慮非同步io的epoll方式的非阻塞伺服器端為了接收文件,臨時把socket設為阻塞,做法正確嗎?
學習 shell 有什麼好書推薦?
Linux 為什麼嚴格區分大小寫?
Linux 中 mmap() 函數的內存映射問題理解?

TAG:Linux | Linux开发 | 源代码 |