實驗要求
1、找一個系統呼叫,系統呼叫號為學號最後2位相同的系統呼叫
2、通過彙編指令觸發該系統呼叫
3、通過gdb跟蹤該系統呼叫的核心處理過程
4、重點閱讀分析系統呼叫入口的儲存現場、恢復現場和系統呼叫返回,以及重點關注系統呼叫過程中核心堆疊狀態的變化
實驗環境及配置
VMware® Workstation 15 Pro
Ubuntu 16.04.3 LTS
64位作業系統
一、基本理論
1、Linux 的系統呼叫
當使用者態程式呼叫一個系統呼叫時,CPU切換到核心態並開始執行 system_call (entry_INT80_32 或 entry_SYSCALL_64) 彙編程式碼,其中根據系統呼叫號呼叫對應的核心處理函式。
具體來說,進入核心後,開始執行對應的中斷服務程式 entry_INT80_32 或者 entry_SYSCALL_64。
2、觸發系統呼叫的方法
(1)使用C庫函式觸發系統呼叫
以time系統呼叫為例:
(2)使用 int &0x80 或者 syscall 彙編程式碼觸發系統呼叫
以time系統呼叫為例。
32位系統:
64位系統:
二、通過彙編指令觸發一個系統呼叫
1、選擇一個系統呼叫
(1)步驟:
Linux原始碼中的 syscall_32.tbl 和 syscall_64.tbl 分別定義了 32位x86 和 64位x86-64的系統呼叫核心處理函式。
由於我的 Linux 系統是64位的,所以進入Linux原始碼中:
~/arch/x86/entry/syscalls/syscall_64.tbl
可以檢視系統呼叫表,如下圖所示:
我的學號最後兩位為50,所以選擇 50號 系統呼叫。
(2)listen 函式
a. 作用
listen 函式用於監聽來自客戶端的 tcp socket 的連線請求,一般在呼叫 bind 函式之後、呼叫 accept 函式之前呼叫 listen 函式。
b. 函式原型
#include <sys/socket.h> int listen(int sockfd, int backlog)
引數 sockfd:被 listen 函式作用的套接字
引數 backlog:偵聽佇列的長度
返回值:
成功 | 失敗 | 錯誤資訊 |
0 | -1 |
EADDRINUSE:另一個socket 也在監聽同一個埠 EBADF:引數sockfd為非法的檔案描述符。 ENOTSOCK:引數sockfd不是檔案描述符。 EOPNOTSUPP:套接字型別不支援listen操作 |
2、通過彙編指令觸發系統呼叫
(1)新建伺服器端程式:server.c
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> int main() { int sockfd,new_fd,listen_result; struct sockaddr_in my_addr; struct sockaddr_in their_addr; int sin_size; //建立TCP套介面 if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { printf("create socket error"); perror("socket"); exit(1); } //初始化結構體,並繫結2323埠 my_addr.sin_family = AF_INET; my_addr.sin_port = htons(2328); my_addr.sin_addr.s_addr = INADDR_ANY; bzero(&(my_addr.sin_zero),8); //繫結套介面 if(bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))==-1) { perror("bind socket error"); exit(1); } //建立監聽套介面, 監聽佇列長度為10 //listen_result = listen(sockfd,10); asm volatile( "movl $0xa,%%edi\n\t" //listen函式的第二個引數 "movl %1,%%edi\n\t" //listen函式的第一個引數 "movl $0x32,%%eax\n\t" //將系統呼叫號50存入eax暫存器 "syscall\n\t" "movq %%rax,%0\n\t" :"=m"(listen_result) :"g"(sockfd) ); if(listen_result == 0) { printf("listen is being called\n"); } if(listen_result ==-1) { perror("listen"); exit(1); } //等待連線 while(1) { sin_size = sizeof(struct sockaddr_in); printf("server is run.\n"); //如果建立連線,將產生一個全新的套接字 if((new_fd = accept(sockfd,(struct sockaddr *)&their_addr,&sin_size))==-1) { perror("accept"); exit(1); } printf("accept success.\n"); //生成一個子程式來完成和客戶端的會話,父程式繼續監聽 if(!fork()) { printf("create new thred success.\n"); //讀取客戶端發來的資訊 int numbytes; char buff[256]; memset(buff,0,256); if((numbytes = recv(new_fd,buff,sizeof(buff),0))==-1) { perror("recv"); exit(1); } printf("%s",buff); //將從客戶端接收到的資訊再發回客戶端 if(send(new_fd,buff,strlen(buff),0)==-1) perror("send"); close(new_fd); exit(0); } close(new_fd); } close(sockfd); }
其中對 listen() 函式的呼叫採用了內嵌彙編指令的形式,即:
asm volatile( "movl $0xa,%%edi\n\t" //listen函式的第二個引數 "movl %1,%%edi\n\t" //listen函式的第一個引數 "movl $0x32,%%eax\n\t" //將系統呼叫號50存入eax暫存器 "syscall\n\t" "movq %%rax,%0\n\t" :"=m"(listen_result) :"g"(sockfd) );
asm volatile 內聯彙編格式
asm volatile(
"Instruction List"
: Output
: Input
: Clobber/Modify
);
a. asm 用來宣告一個內聯彙編表示式,任何內聯彙編表示式都是以它開頭,必不可少。
b. volatile 是可選的,如果選用,則向GCC宣告不對該內聯彙編進行優化。
c. Instruction List 是彙編指令序列,如果有多條指令時:
可以將多條指令放在一隊引號中,用 ; 或者 \n 將它們分開;
也可以一條指令放在一對引號中,每條指令一行。
d. Output 用來指定內聯彙編語句的輸出,相當於系統函式的返回值,格式為:
"=a"(initval)
e. Input 用來指定當前內聯彙編語句的輸入,相當於系統函式的引數(當該引數為使用C語言的變數的值時,採用這種方法),格式為:
"constraint(variable)"
可以看到,如果使用庫函式觸發函式呼叫的話,應該是被註釋掉的語句:
listen_result = listen(sockfd,10);
該函式有兩個引數,分別是變數 sockfd 和 常量10,返回值為 listen_result,按照上述規定完成彙編指令觸發系統呼叫。
(2)新建客戶端程式:client.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> int main(int argc,char *argv[]) { int sockfd,numbytes; char buf[100]; struct sockaddr_in their_addr;
//建立一個TCP套介面 if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) { perror("socket"); printf("create socket error.建立一個TCP套介面失敗"); exit(1); } //初始化結構體,連線到伺服器的2323埠 their_addr.sin_family = AF_INET; their_addr.sin_port = htons(2328); // their_addr.sin_addr = *((struct in_addr *)he->h_addr); inet_aton( "127.0.0.1", &their_addr.sin_addr ); bzero(&(their_addr.sin_zero),8); //和伺服器建立連線 if(connect(sockfd,(struct sockaddr *)&their_addr,sizeof(struct sockaddr))==-1) { perror("connect"); exit(1); } //向伺服器傳送資料 if(send(sockfd,"hello!socket.",6,0)==-1) { perror("send"); exit(1); } //接受從伺服器返回的資訊 if((numbytes = recv(sockfd,buf,100,0))==-1) { perror("recv"); exit(1); } buf[numbytes] = '/0'; printf("Recive from server:%s",buf); //關閉socket close(sockfd); return 0; }
(3)對兩個程式分別編譯、連結
a. 程式碼如下:
gcc -o server server.c -static gcc -o client client.c -static
格式:gcc -o file file.c
將檔案 file.c 編譯成可執行檔案 file
引數 -static:強制使用靜態庫連結
引數 -m32:在64位機器上輸出32位程式碼時,需要加上 -32
b. 結果如下:
執行程式碼前:
可以看出資料夾中目前只有 server.c 和 client.c。
執行程式碼後:
發現資料夾中已經生成了我們想要的可執行檔案 server 和 client。
(4)執行可執行檔案
a. 啟動 server,表明伺服器端啟動
程式碼如下:
sudo ./server
伺服器端啟動,結果如下:
可以看到輸出 “listen is being called”,表明我們想要呼叫的系統函式 listen() 已經被成功觸發,即系統呼叫成功。
此時伺服器端就等待客戶端與其建立連結並通訊。
b. 再啟動一個終端充當客戶端,在該終端中啟動 client,表明客戶端啟動
程式碼如下:
sudo ./client
客戶端啟動,結果如下:
可以看到客戶端的終端輸出 ”Recive from server:hello!0",表明客戶端與伺服器端已成功建立連線,並且客戶端收到了伺服器端發回的資訊。
c. 此時,伺服器端的資訊為:
伺服器端繼續 listen 來自客戶端的資訊。
如果我們再在另外一個終端內使用 sudo ./client 啟動一個客戶端,伺服器端也會有相應啟動成功的資訊生成:
三、通過gdb跟蹤該系統呼叫的核心處理過程
1、環境配置
(1)安裝開發工具
sudo apt install build-essential sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev sudo apt install axel
以上工具在第一次實驗時已經進行了安裝。
(2)下載核心原始碼
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz xz -d linux-5.4.34.tar.xz tar -xvf linux-5.4.34.tar cd linux-5.4.34
(3)配置核心選項
make defconfig # Default configuration is based on 'x86_64_defconfig' make menuconfig # 開啟debug相關選項 Kernel hacking ---> Compile-time checks and compiler options ---> [*] Compile the kernel with debug info [*] Provide GDB scripts for kernel debugging [*] Kernel debugging # 關閉KASLR,否則會導致打斷點失敗 Processor type and features ----> [] Randomize the address of the kernel image (KASLR)
(4)編譯核心
make -j$(nproc) # nproc gives the number of CPU cores/threads available
(5)啟動qemu
#測試⼀下核心能不能正常載入運⾏,因為沒有⽂件系統最終會kernel panic
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
(6)製作記憶體根檔案系統
a. 下載解壓:
axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2 tar -jxvf busybox-1.31.1.tar.bz2 cd busybox-1.31.1
b. 配置編譯、安裝:
make menuconfig #記得要編譯成靜態連結,不⽤動態連結庫。 Settings ---> [*] Build static binary (no shared libs) #然後編譯安裝,預設會安裝到原始碼⽬錄下的 _install ⽬錄中。 make -j$(nproc) && make install
c. 製作記憶體根檔案系統映象:
在 linux-5.4.34 目錄下建立 rootfs 資料夾
mkdir rootfs cd rootfs cp ../busybox-1.31.1/_install/* ./ -rf mkdir dev proc sys home sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
d. 準備 init 指令碼檔案放在根檔案系統根目錄下(rootfs/init):
新建名為 init 的文件檔案,新增如下內容到init檔案
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome Liu JianingOS!" echo "--------------------" cd home /bin/sh
給init指令碼新增可執行許可權
chmod +x init
e. 打包成記憶體根檔案系統映象
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
f. 測試掛在根檔案系統,看核心啟動完成後是否執行 init 指令碼
返回到 linux-5.4.34目錄下,啟動qemu
qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz
結果如下:
說明 init 指令碼被執行。
2、跟蹤除錯 Linux 核心
(1)根據第二部分的內容編寫利用匯編指令觸發系統呼叫的程式碼
在 rootfs/home 目錄下分別建立兩個名為 server.c 和 client.c 的檔案,並存入第二部分相應的程式碼。
(2)使用 gcc 編譯成可執行檔案 server 和 client
gcc -o server server.c -static gcc -o client client.c -static
(3)重新打包記憶體根檔案系統映象
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
(4)使用 gdb 跟蹤除錯
方法:
使用 gdb 跟蹤除錯核心時,在啟動 qemu 命令上新增兩個引數:
a. -s
作用:
- 在TCP 1234 埠上建立了一個 gdb-server(如果不想使用1234埠,可以用 -gdb tcp:xxxx 來替代 -s 選項)
- 開啟另外一個視窗,用 gdb 把帶符號表的核心映象 vmlinux 載入進來
- 然後連線 gdb server,設定斷點跟蹤核心
b. -S
作用:
- 表示啟動時暫停虛擬機器,等待 gdb 執行 continue 指令(可以簡寫為c)。
步驟:
a. 使用純命令列啟動 qemu
qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
用該命令啟動qemu,可以看到虛擬機器一啟動就暫停了,終端停留在下面的介面:
引數:-nographic -append "console=ttyS0"
啟動時不會彈出 qemu 虛擬機器視窗,可以在純命令列下啟動虛擬機器。
【可以通過 killall qemu-system-x86_64 命令強制關閉虛擬機器】
b. 在開啟一個終端視窗,進入 linux-5.4.34 目錄下,載入核心映象:
gdb vmlinux
c. 連線 gdb server,即在 gdb 中執行下方程式碼:
(gdb) target remote:1234
d. 給文章中使用的系統呼叫設定斷點
方法:
(gdb) b 系統呼叫函式名
上文可知,我選擇的系統呼叫函式為 listen(),具體資訊如下:
程式碼如下:
(gdb) b __x64_sys_listen
e. 輸入 (gdb) c 指令繼續執行程式
此時,第一個開啟的終端的內容為:
f. 執行編譯好的可執行程式碼 server,使用 gdb 進行單步除錯
在第一個終端中輸入如下程式碼:
/home # ls
/home # ./server
此時第二個終端內容為:
在第二個終端中輸入:
(gdb) n
結果為:
報錯:
GDB 遠端除錯錯誤:Remote 'g' packet reply is too long
解決方法:
重新下載 gdb,並修改其中 remote.c 檔案內容
由 http://ftp.gnu.org/gnu/gdb/ 下載 gdb的較新版本,此處我下載的是 gdb-7.8.tar.gz,並將其放在了 /home/linux 目錄下
進入 /home/linux 目錄下,對該檔案進行解壓縮
tar zxvf gdb-7.8.tar.gz
修改 gdb-7.8/gdb 目錄下的 remote.c 檔案內容:
if (buf_len > 2 * rsa->sizeof_g_packet) { rsa->sizeof_g_packet = buf_len ; for (i = 0; i < gdbarch_num_regs (gdbarch); i++) { if (rsa->regs->pnum == -1) continue; if (rsa->regs->offset >= rsa->sizeof_g_packet) rsa->regs->in_g_packet = 0; else rsa->regs->in_g_packet = 1; } }
在 gdb-7.8 目錄下執行以下命令安裝 gdb:
./configure
make
make install
至此,我們再重複上述步驟就可以使用 gdb 對程式設定斷點,並且進行單步除錯。
(5)使用 gdb 對程式進行單步除錯
gdb操作指令:
(gdb) l 檢視程式碼情況
(gdb) n 單步執行
(gdb) step 進入函式內部
(gdb) bt 檢視堆疊
重新安裝並調整 gdb 之後,按照步驟(4)中的 a - f 依次執行。
a. 當第一個終端執行可執行檔案server之後,即:
/home # ./server
第二個終端內容為:
可以看出斷點位置。
b. 檢視堆疊資訊
在第二個終端中輸入命令:
(gdb) bt
檢視當前堆疊資訊,如下所示:
c. 單步除錯
在第二個終端輸入如下命令,進行單步除錯:
(gdb) n
結果如下:
四、分析總結
1、使用 (gdb) bt 檢視當前堆疊情況
根據結果顯示,函式呼叫可以分為4層:
頂層: __x64_sys_listen 作用:開放給使用者態使用的系統呼叫函式介面
第二層:do_syscall_64 作用:獲取系統呼叫號,從而呼叫系統函式
第三層:entry_syscall_64 作用:儲存現場工作,呼叫第二層的 do_syscall_64
第四層:作業系統
2、根據單步除錯結果從頂層往下依次檢視
(1)斷點定位
斷點定位為:
/home/linux/linux-5.4.34/net/socket.c 的1688行
執行以下程式碼,前往相應位置檢視:
cd linux/linux-5.4.34/net cat -n socket.c
結果為:
進入 __sys_listen(fd, backlog) 函式檢視:
int __sys_listen(int fd, int backlog) { struct socket *sock; int err, fput_needed; int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; if ((unsigned int)backlog > somaxconn) backlog = somaxconn; err = security_socket_listen(sock, backlog); if (!err) err = sock->ops->listen(sock, backlog); fput_light(sock->file, fput_needed); } return err; }
(2)執行 do_syscall_64 函式
該函式定位在:
/home/linux/linux-5.4.34/arch/x86/entry/common.c 的第300行
(3)執行 entry_SYSCALL_64 函式
該函式定位在:
/home/linux/linux-5.4.34/arch/x86/entry/entry_64.S 的第184行
3、系統呼叫總結
(1)使用者態的程式程式碼 server.c 中的內嵌彙編指令 syscall 觸發系統呼叫
(2)通過 MSR 暫存器找到函式入口
中斷函式入口為:
/home/linux/linux/-5.4.34/arch/entry/entry_64.S 第145行 ENTRY(entry_SYSCALL_64) 函式,這個函式為 x86_64 系統進行系統呼叫的通用入口。
ENTRY函式如下:
a. swapgs
使用 swapgs 指令和 下面一系列的壓棧動作來儲存現場。
b. call do_syscall_64
呼叫 do_syscall_64 查詢系統呼叫表,獲得所要使用的系統呼叫號。
(3)跳轉執行 do_syscall_64
跳轉到 /home/linux/linux-5.4.34/x86/entry/common.c 下的 do_system_64函式
a. regs->ax = sys_call_table[nr](regs)
從系統呼叫表中獲得系統呼叫號,並將其存在到 ax 暫存器中,然後去執行系統呼叫函式。
b. syscall_return_slowpath(regs)
用於系統呼叫函式執行結束後,恢復現場
(4)跳轉執行系統系統函式 listen
跳轉到 /home/linux/linux-5.4.34/net/socket.c 函式,開始執行函式;
(5)恢復現場
函式執行完成後,需要進行現場恢復,因此再次回到:
/home/linux/linux/-5.4.34/arch/x86/entry/entry_64.S
進行現場的恢復。
至此,整個系統呼叫完成。
參考文章:
https://blog.csdn.net/u013920085/article/details/20574249
https://blog.csdn.net/yangbodong22011/article/details/60399728