某模塊作為前台進程獨立運行時,運行命令攜帶命令行參數;作為某平台下守護進程子進程運行時,需要將命令行參數固化在代碼里。類似如下寫法:
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
先不管注釋內容,直接找到__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 }
可見該函數對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!