• 关于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参数尾部增加了结束符('')。此刻真相大白,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

  • 相关阅读:
    Linux入门
    服务器核心知识
    跨域
    DRF的解析器和渲染器
    DRF的分页
    DRF 权限 频率
    DRF 版本 认证
    Django Rest Framework 视图和路由
    Serializers 序列化组件
    六、Java NIO 通道之间的数据传输
  • 原文地址:https://www.cnblogs.com/clover-toeic/p/3741689.html
Copyright © 2020-2023  润新知