關於Linux系統basename函數缺陷的思考


 

     某模塊作為前台進程獨立運行時,運行命令攜帶命令行參數;作為某平台下守護進程子進程運行時,需要將命令行參數固化在代碼里。類似如下寫法:

char *argv[] = {"./DslDriver", "-t", "/bin/VdslModemSco.bin"};

int argc = sizeof(argv) / sizeof(argv[0]);

     隨后,調用basename函數(頭文件為libgen.h)解析argv[0],即"./DslDriver"。實測發現,在Linux原生系統中解析正常,在某平台下解析時則會發生段錯誤。

     合理的想法自然是懷疑兩種環境下basename函數的實現不同。Linux原生函數源碼未找到,但某平台uclibc源碼中可以找到basename函數的實現:

1 /* Return final component of PATH.
2    This is the weird XPG version of this function.  It sometimes will
3    modify its argument.  Therefore we normally use the GNU version (in
4    <string.h>) and only if this header is included make the XPG
5    version available under the real name.  */
6 extern char *__xpg_basename (char *__path) __THROW;
7 #define basename    __xpg_basename
Libgen.h

     先不管注釋內容,直接找到__xpg_basename定義處:

 1 char *__xpg_basename(register char *path)
 2 {
 3     static const char null_or_empty[] = ".";
 4     char *first;
 5     register char *last;
 6 
 7     first = (char *) null_or_empty;
 8 
 9     if (path && *path) {
10         first = path;
11         last = path - 1;
12 
13         do {
14             if ((*path != '/') && (path > ++last)) {
15                 last = first = path;
16             }
17         } while (*++path);
18 
19         if (*first == '/') {
20             last = first;
21         }
22         last[1] = 0; //注意此句!
23     }
24 
25     return first;
26 }
Wstring.c

     可見該函數對path參數尾部增加了結束符('\0')。此刻真相大白,basename("./DslDriver")傳入的是只讀字符串,導致basename函數內試圖修改只讀數據區,當然會發生段錯誤。而運行命令中,命令行參數並非只讀數據(實為字符數組),因此不會發生段錯誤。

     若要在某平台下安全使用basename函數,有兩種改法:

     1) 傳入字符數組。如下:

char argv[][sizeof("/bin/VdslModemSco.bin")] = {"./DslDriver", "-t", "/bin/VdslModemSco.bin"};

     2) 改用GNU的實現版本(頭文件為string.h)。GNU版本絕不會更改它的參數,因此可正確處理靜態字符串。

 

【思考】

     回想一下,basename內有必要對path增加結束符嗎?個人覺得沒必要,這樣反會限制函數的應用場景。理論上講,若path為只讀字符串,說明調用者確知path內容,手工剝離即可,無需調用basename函數(源碼的“合理”之處)。只是本模塊的使用方式較為特殊。另一方面,該缺陷也警醒編碼者,不要"自作多情"地memset入參內容(使之失去累積性),或對入參字符串添加結束符。這些細節還是留待調用者自由發揮罷~

     再舉一例說明接口函數的設計,如下消息發送函數:

FUNC_STATUS SendSsynMsg(VOID* pvInMsg, INT16U wInMsgLen, VOID* pvOutMsg, INT16U* pwOutMsgLen);

     原始接口要求pvInMsg指針不為空(說明消息體有內容)時,wInMsgLen不能為0。同時,pvOutMsg和pwOutMsgLen指針不能為空,且pwOutMsgLen指向的值(出參長度)不能為0,用以向調用者反饋信息(如Ack)。

     其實根據實際使用場景,並不需要限制出參。若調用者不想獲得反饋,則應允許pvOutMsg和pwOutMsgLen指針為空。這樣,可降低調用的復雜度和出錯率。此外,允許pvInMsg指針不為空時wInMsgLen為0,並在接口內按照緩沖區的最大長度拷貝pvInMsg,也可降低調用的出錯率(尤其是當wInMsgLen參數為變量而非標量時)——消息接收方必然知道相應消息體內容的長度(否則如何解析?)。

【Tips

     如何查找庫函數的原型定義所在處?

     很簡單,在調用處"改寫"函數聲明。例如,期望函數原型為int Func(char *)——通常可由調用方式猜出,則在調用前聲明為int Func(int)。編譯器會通過conflicting types錯誤來指示previous declaration of TheLibFunc

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM