程式扎記: [Linux 文章收集] strace 命令用法

標籤

2015年3月23日 星期一

[Linux 文章收集] strace 命令用法

Source From Here 
Preface 
strace 用來跟踪一個進程的系統調用或信號產生的情況, 通常的用法是 strace 執行一直到 commande 結束. 並且將所呼叫的系統呼叫的名稱、參數和返回值輸出到標準輸出或者輸出到-o 指定的文件. strace 是一個功能強大的除錯,分析診斷工具.你將發現他是一個極好的幫手在你要除錯一個無法看到原始碼或者原始碼無法在編譯的程序. 

你將輕鬆的學習到一個軟體是如何通過系統呼叫來實現他的功能的.而且作為一個程序設計師,你可以了解到在用戶態和核心態是如何通過系統呼叫和信號來實現程序的功能的. 
strace的每一行輸出包括系統呼叫名稱,然後是參數和返回值. 底下這個例子: 
$ strace cat /dev/null
...
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0

open("/dev/null", O_RDONLY) = 3
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
read(3, "", 32768) = 0
close(3) = 0
close(1) = 0
close(2) = 0
exit_group(0) = ?

或當你嘗試 "ls -l" 一個不存在的文件時,會有: 
$ strace ls -l noexist
...
close(3) = 0
futex(0x35ce58ef88, FUTEX_WAKE_PRIVATE, 2147483647) = 0
lstat("noexist", 0x1943810) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/en_US.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
...

可以讓你簡單知道 strace 輸出的格式與內容. 

用 strace 除錯程序 
在理想世界裡,每當一個程序不能正常執行一個功能時,它就會給出一個有用的錯誤提示,告訴你在足夠的改正錯誤的線索。但遺憾的是,我們不是生活在理想世界裡,起碼不總是生活在理想世界裡。有時候一個程序出現了問題,你無法找到原因。 

這就是除錯程序出現的原因。 strace是一個必不可少的除錯工具,strace用來監視系統呼叫。你不僅可以除錯一個新開始的程序,也可以除錯一個已經在運行的程序(把 strace 綁定到一個已有的 PID 上面)。首先讓我們看一個真實的例子: 

啟動 KDE 時出現問題 
前一段時間,我在啟動KDE的時候出了問題,KDE的錯誤信息無法給我任何有幫助的線索: 
_KDE_IceTransSocketCreateListener: failed to bind listener
_KDE_IceTransSocketUNIXCreateListener: ...SocketCreateListener() failed
_KDE_IceTransMakeAllCOTSServerListeners: failed to create listener for local

Cannot establish any listening sockets DCOPServer self-test failed.

對我來說這個錯誤信息沒有太多意義,只是一個對KDE來說至關重要的負責進程間通信的程序無法啟動。我還可以知道這個錯誤和ICE協議(Inter Client Exchange)有關,除此之外,我不知道什麼是KDE啟動出錯的原因。 

我決定採用 strace 看一下在啟動 dcopserver 時到底程序做了什麼: 
# -o filename: 將 strace 的輸出寫入文件 filename
# -f 跟踪由fork呼叫所產生的子進程.
# -F 嘗試跟踪vfork呼叫.在-f時,vfork不被跟踪. 

$ strace -f -F -o ~/dcop-strace.txt dcopserver

再次出現錯誤之後,我檢查了錯誤輸出文件 dcop-strace.txt,文件裡有很多系統呼叫的記錄。在程序運行出錯前的有關記錄如下: 
  1. 27207 mkdir("/tmp/.ICE-unix"0777) = -1 EEXIST (File exists)  
  2. 27207 lstat64("/tmp/.ICE-unix", {st_mode=S_IFDIR|S_ISVTX|0755, st_size=4096, ...}) = 0  
  3. 27207 unlink("/tmp/.ICE-unix/dcop27207-1066844596") = -1 ENOENT (No such file or directory)  
  4. 27207 bind(3, {sin_family=AF_UNIX, path="/tmp/.ICE-unix/dcop27207-1066844596"}, 38) = -1 EACCES (Permission denied)   
  5. 27207 write(2"_KDE_IceTrans"13) = 13  
  6. 27207 write(2"SocketCreateListener: failed to "..., 46) = 46  
  7. 27207 close(3) = 0 27207 write(2"_KDE_IceTrans"13) = 13  
  8. 27207 write(2"SocketUNIXCreateListener: ...Soc"..., 59) = 59  
  9. 27207 umask(0) = 0 27207 write(2"_KDE_IceTrans"13) = 13  
  10. 27207 write(2"MakeAllCOTSServerListeners: fail"..., 64) = 64  
  11. 27207 write(2"Cannot establish any listening s"..., 39) = 39  
其中第一行顯示程序試圖創建/tmp/.ICE-unix目錄,權限為0777,這個操作因為目錄已經存在而失敗了。第二個系統呼叫(lstat64)檢查了目錄狀態,並顯示這個目錄的權限是0755,這裡出現了第一個程序運行錯誤的線索:程序試圖創建屬性為0777的目錄,但是已經存在了一個屬性為0755的目錄。第三個系統呼叫(unlink)試圖刪除一個文件,但是這個文件並不存在。這並不奇怪,因為這個操作只是試圖刪掉可能存在的老文件。 

但是,第四行確認了錯誤所在。他試圖綁定到 /tmp/.ICE-unix/dcop27207-1066844596,但是出現了拒絕訪問錯誤。 . ICE_unix 目錄的用戶和組都是 root,並且只有所有者俱有寫權限。一個非 root 用戶無法在這個目錄下面建立文件,如果把目錄屬性改成 0777,則前面的操作有可能可以執行,而這正是第一步錯誤出現時進行過的操作。 

所以我運行了 chmod 0777 /tmp/.ICE-unix 之後 KDE 就可以正常啟動了,問題解決了,用 strace 進行跟踪除錯只需要花很短的幾分鐘時間跟踪程序運行,然後檢查並分析輸出文件。 

說明:運行chmod 0777只是一個測試,一般不要把一個目錄設置成所有用戶可讀寫,同時不設置粘滯位 (sticky bit)。給目錄設置粘滯位可以阻止一個用戶隨意刪除可寫目錄下面其他人的文件。一般你會發現 /tmp 目錄因為這個原因設置了粘滯位。 KDE可以正常啟動之後,運行 chmod +t /tmp/.ICE-unix 給 .ICE_unix 設置粘滯位。 

解決庫依賴問題 
starce 的另一個用處是解決和動態庫相關的問題。當對一個可執行文件運行 ldd 時,它會告訴你程序使用的動態庫和找到動態庫的位置。但是如果你正在使用一個比較老的 glibc 版本(2.2或更早),你可能會有一個有 bug 的 ldd 程序,它可能會報告在一個目錄下發現一個動態庫,但是真正運行程序時動態連接程序(/lib/ld-linux.so.2)卻可能到另外一個目錄去找動態連接庫。這通常因為 /etc/ld.so.conf /etc/ld.so.cache 文件不一致,或者 /etc/ld.so.cache 被破壞。在 glibc 2.3.2 版本上這個錯誤不會出現。 

儘管這樣,ldd 並不能把所有程序依賴的動態庫列出來,系統呼叫 dlopen 可以在需要的時候自動調入需要的動態庫,而這些庫可能不會被 ldd 列出來。作為 glibc 的一部分的NSS(Name Server Switch)庫就是一個典型的例子,NSS 的一個作用就是告訴應用程序到哪裡去尋找系統帳號數據庫。應用程序不會直接連接到 NSS 庫,glibc 則會通過 dlopen 自動調入 NSS 庫。如果這樣的庫偶然丟失,你不會被告知存在庫依賴問題,但這樣的程序就無法通過用戶名解析得到用戶ID了。讓我們看一個例子: 

whoami 程序會給出你自己的用戶名,這個程序在一些需要知道運行程序的真正用戶的腳本程序裡面非常有用,whoami 的一個示例輸出如下 
$ whoami
root


用調試工具掌握軟件的工作原理 
用調試工具實時跟踪軟件的運行情況不僅是診斷軟件"疑難雜症"的有效的手段,也可幫助我們理清軟件的"脈絡",即快速掌握軟件的運行流程和工作原理,不失為一種學習源代碼的輔助方法。下面這個案例展現瞭如何使用strace通過跟踪別的軟件來"觸發靈感",從而解決軟件開發中的難題的。 

在進程內打開一個文件,都有唯一一個文件描述符(fd:file descriptor)與這個文件對應。而本人在開發一個軟件過程中遇到這樣一個問題:已知一個 fd ,如何獲取這個 fd 所對應文件的完整路徑?不管是 Linux、FreeBSD 或是其它 Unix 系統都沒有提供這樣的API,怎麼辦呢?我們換個角度思考:Unix 下有沒有什麼軟件可以獲取進程打開了哪些文件?如果你經驗足夠豐富,很容易想到 lsof,使用它既可以知道進程打開了哪些文件,也可以了解一個文件被哪個進程打開。 

好,我們用一個小程序來試驗一下 lsof,看它是如何獲取進程打開了哪些文件。 
  1. /* testlsof.c */  
  2. #include   
  3. #include   
  4. #include   
  5. #include   
  6. #include   
  7. int main(void)  
  8. {  
  9.         open("/tmp/foo", O_CREAT|O_RDONLY); /* 打開文件/tmp/foo */  
  10.         sleep(1200); /* 睡眠1200秒,以便進行後續操作*/  
  11.         return 0;  
  12. }  
將 testlsof 放入後台運行,其 pid 為3125。命令 lsof -p 3125 查看進程 3125 打開了哪些文件,我們用 strace 跟踪 lsof 的運行,輸出結果保存在 lsof.strace 中: 
# gcc testlsof.c -o testlsof
# vi /tmp/foo
# ./testlsof &

我們以 "/tmp/foo" 為關鍵字搜索輸出文件 lsof.strace,結果只有一條: 
# grep '/tmp/foo' lsof.strace
readlink("/proc/2854/fd/3", "/tmp/foo"..., 4096) = 8

原來 lsof 巧妙的利用了/proc/nnnn/fd/ 目錄(nnnn 為 pid):Linux 內核會為每一個進程在 /proc/ 建立一個以其 pid 為名的目錄用來保存進程的相關信息,而其子目錄 fd 保存的是該進程打開的所有文件的 fd。目標離我們很近了。好,我們到 /proc/2854/fd/ 看個究竟: 
$ ll /proc/3029/fd
total 0
lrwx------. 1 root root 64 Jan 6 07:34 0 -> /dev/pts/1
lrwx------. 1 root root 64 Jan 6 07:34 1 -> /dev/pts/1
lrwx------. 1 root root 64 Jan 6 07:34 2 -> /dev/pts/1
lr-x------. 1 root root 64 Jan 6 07:34 3 -> /tmp/foo

答案已經很明顯了:/proc/nnnn/fd/ 目錄下的每一個 fd 文件都是符號鏈接,而此鏈接就指向被該進程打開的一個文件。我們只要用 readlink() 系統調用就可以獲取某個fd對應的文件了,代碼如下: 
  1. #include   
  2. #include   
  3. #include   
  4. #include   
  5. #include   
  6. #include   
  7. int get_pathname_from_fd(int fd, char pathname[], int n)  
  8. {  
  9.         char buf[1024];  
  10.         pid_t pid;  
  11.         bzero(buf, 1024);  
  12.         pid = getpid();  
  13.         snprintf(buf, 1024"/proc/%i/fd/%i", pid, fd);  
  14.         return readlink(buf, pathname, n);  
  15. }  
  16. int main(void)  
  17. {  
  18.         int fd;  
  19.         char pathname[4096];  
  20.         bzero(pathname, 4096);  
  21.         fd = open("/tmp/foo", O_CREAT|O_RDONLY);  
  22.         get_pathname_from_fd(fd, pathname, 4096);  
  23.         printf("fd=%d; pathname=%s\n", fd, pathname);  
  24.         return 0;  
  25. }  
Supplement 
技巧: 使用truss、strace或ltrace诊断软件的"疑难杂症"

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!