Linux/Golang/glibC系統調用
本文主要通過分析Linux環境下Golang的系統調用,以此闡明整個流程
有時候涉略過多,反而遭到質疑~,寫點文章證明自己實力也好
Golang系統調用
找個函數來分析
https://pkg.go.dev/os/exec#Cmd.Wait
源碼文件在src/os目錄下的: exec.go
-> exec_unix.go
-> pidfd_linux.go
https://github.com/golang/go/blob/2f6426834c150c37cdb1330b48e9903963d4329c/src/os/exec.go#L134
往下是系統調用: src/syscall目錄的 syscall_linux.go
-> ``
runtime層的:src/internal/runtime/syscall/syscall_linux.go
,如下圖,可以看見Sysacll6只有聲明沒有函數體,是個外部聲明。
其函數體內容實際上位於同目錄下的 .s 彙編文件,與編譯時採用的架構工具鏈相關。
- 如編譯工具鏈採用amd64則會編譯鏈接位於 src/internal/runtime/syscall/asm_linux_amd64.s 的彙編文件
- 如編譯工具鏈採用arm64,則會編譯鏈接 src/internal/runtime/syscall/asm_linux_arm64.s 的彙編文件
by the way: 這裏的語法是Golang彙編,屬於Plan9分支。
golang彙編參考資料:
總結:Golang直接了當地使用匯編實現了系統調用(軟中斷號),而不需要再通過 libc 去調用系統調用庫。這樣的好處是不需要考慮 glibc 繁雜沉重的兼容性方案。
Linux 定義的系統調用表
- 很全的表格:https://gpages.juszkiewicz.com.pl/syscalls-table/syscalls.html
- ARM64架構:
- x86_32位架構tbl系統調用表: https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl
- x86_64位架構tbl系統調用表: https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl
本地審計工具:ausyscall --dump
Linux系統調用
內核實現
通過軟中斷陷入內核態/特權模式
和STM32 ARM核心一樣,都是由一個異常向量表描述中斷對應的Handler地址,軟硬中斷也是一樣。
系統調用函數在 include/linux/syscalls.h中定義
我們拿asmlinkage long sys_openat(int dfd, const char __user *filename, int flags, umode_t mode);
來分析
這裏使用了彙編鏈接,它和上文提到的tbl系統調用表有關。我們拿x86/i386分析,arch/x86/entry/syscalls/syscall_32.tbl
中斷號 295 架構i386即傳統32位x86 sys_openat 是其回調函數/軟中斷Handler
linux/include/uapi/asm-generic/unistd.h
其實現位於 arch/處理器架構/include/之下
可以在 arch/x86 下搜索 openat
關於內核的系統調用這部分,本人會在再出一個文章。
Glibc 系統調用庫
注意:Glibc屬於庫,不屬於內核,是根文件系統的一部分。
我們在應用態陷入內核態,使用的c庫裏的open()等等函數,最後都是鏈式調用到了syscall()類的系統調用函數。
看glibc的源碼,就會發現彎彎繞繞,最後是調用到
-
x86彙編 https://github.com/bminor/glibc/tree/master/sysdeps/unix/x86_64
-
arm彙編 https://github.com/bminor/glibc/tree/master/sysdeps/unix/arm
-
sysdeps/unix/arm/sysdep.S
-
sysdeps/unix/arm/sysdep.h
-
作用是將參數寫入寄存器,讓SoC自己觸發軟中斷,根據Linux內核註冊的軟中斷號執行對應地址段的函數,也就是我們常在STM32裏註冊定義的中斷的handle函數。
Linux應用態到內核態例子
在線閱讀代碼:
- https://elixir.bootlin.com/glibc/glibc-2.29/source/include/errno.h#L37
- https://codebrowser.dev/glibc/glibc/io/read.c.html
- 帶了編譯產物的倉庫 https://github.com/bminor/glibc/tree/a81cdde1cb9d514fc8f014ddf21771c96ff2c182
這些在線網站都不錯,但爲了高亮,所以我截圖放了github的
我們在應用層調用 系統庫的 fread()函數
其鏈接到glibc庫的 libio/iofread.c
其中第44行可見其爲 _IO_fread
聲明瞭weak弱鏈接別名 fread
,有關別名表可見編譯產物如sysdeps/unix/syscalls.list
等
做了一些預操作之後,調用libio/libio.h 聲明的 libio/genops.c:_IO_sgetn
宏定義 libio/libioP.h
ps: JUMP2代表兩個參數
展開宏
展開宏
展開宏
展開宏
結構體
也就說調用了 FP.__xsgetn(FP, DATA, N) ,展開差不多是
struct _IO_FILE_plus *THIS;
THIS->vtable->__xsgetn; 即_IO_xsgetn_t類型函數指針
即THIS/file對象的函數地址 size_t __xsgetn (FILE *FP, void *DATA, size_t N);
初始化 _IO_jump_t 位於 JUMP_INIT
宏展開
函數定義
相關的計數器
再看另一個,我們常用的fopen
compat_symbol (libc, _IO_old_file_fopen, _IO_file_fopen, GLIBC_2_0);
#define __open open
這裏定向到了 open, 我們需要通過編譯產物 sysdeps/unix/syscalls.list 找到其鏈接段
可在 io/open.c 找到函數 __libc_open
位於
由於弱定義,所以被以下覆蓋 sysdeps/unix/sysv/linux/open.c
關鍵在於第43行的SYSCALL_CANCEL
,其中的宏
展開宏INLINE_SYSCALL_CALL
展開宏
展開宏
展開宏
展開爲
//展開
__INLINE_SYSCALL4(openat, AT_FDCWD, file, oflag, mode)
//繼續展開爲
__INLINE_SYSCALL(openat, 4, AT_FDCWD, file, oflag, mode)
//x86
#define SYS_ify(syscall_name) __NR_##syscall_name
#define INTERNAL_SYSCALL(name, nr, args...) \
internal_syscall##nr (SYS_ify (name), args)
//aarch64即 arm64
# define INTERNAL_SYSCALL_AARCH64(name, nr, args...) \
INTERNAL_SYSCALL_RAW(__ARM_NR_##name, nr, args)
x86的展開爲 internal_syscall4 (__NR_openat, AT_FDCWD, file, oflag, mode)
可在 sysdeps/unix/sysv/linux/sh/arch-syscall.h 找到,其中斷號爲 295
對應openat的x86/i386中斷號,剛好就是295,源碼分析完全正確!每種平臺的中斷號都不一樣,但是這樣分析是正確的。
體驗一下GNU宏地獄吧!
而ARM64的就高明得多,直接通過asm彙編指令寫寄存器跳轉執行__libc_do_syscall
完成
glibc/sysdeps/unix/sysv/linux/arm/libc-do-syscall.S
總之是一個系統調用,等價於 openat(AT_FDCWD, file, oflag, mode);
總結:sysdeps是系統調用的實現,向上屏蔽細節,但是封裝的過程用於一堆條件宏,根本沒辦法用代碼分析工具,也難以調試。
對GNU LIBC代碼的個人拙見:
好處:節省空間,較好的運行速度。
壞處:作爲計算機世界的底層支持,這樣還不如在編譯器優化階段下功夫,過多的黑魔法必然寫出難以理解的代碼,牽一髮動全身,沒有人願意去改這堆瘋狂嵌套的代碼。作爲新興語言愛好者的我,始終認爲程序要少點黑魔法,簡潔直接纔是最優解,剩下的東西都應該交給編譯器,何況系統調用的耗時從來就不在這裏,主要性能影響都是在內核態用戶態切換的時候,並不在c庫本身。
GNU的代碼向來很難讀,glibc更是個寄吧,各種宏和硬鏈接亂飛,有再好的代碼閱讀工具也難找出來。
但要說來,說到底還是c語言/鏈接器的設計缺陷,沒辦法更好的實現動態表和靜態表。(多態組合、編譯期間的函數靜態段的多分支鏈接)
微軟的代碼是框架難以理解,因爲他們也不給出架構圖和代碼結構的...,,而GNU的代碼是宏和鏈接難以理解。
最後
請指正批評!感謝閱讀。