2013年四月月 发布的文章

libiconv库链接问题一则

与在Solaris系统上不同,Linux的libc库中包含了libiconv库中函数的定义,因此在Linux上使用libiconv库相关函数,编译时是不需要显式-liconv的。但最近我的一位同事在某redhat enterprise server 5.6机器上编译程序时却遇到了找不到iconv库函数符号的链接问题,到底是怎样一回事呢?这里分享一下问题查找过程。

一、现场重现

这里借用一下这位同事的测试程序以及那台机器,重现一下问题过程:
/*test.c */


#include <iconv.h>

int main(void)
{
    int r;
    char *sin, *sout;
    size_t lenin, lenout;
    char *src = "你好!";
    char dst[256] = {0};
    iconv_t c_pt;  

    sin = src;
    lenin = strlen(src)+1;

    sout = dst;
    lenout = 256;

    if ((c_pt = iconv_open("UTF-8", "GB2312")) == (iconv_t)(-1)){
        printf("iconv_open error!. errno[%d].\n", errno);
        return -1;
    }

    if ((r = iconv(c_pt, (char **)&sin, &lenin, &sout, &lenout)) != 0){
        printf("iconv error!. errno[%d].\n", r);
        return -1;
    }  

    iconv_close(c_pt);

    printf("SRC[%s], DST[%s].\n", src, dst);

    return 0;
}

根据之前的经验,我们按如下命令编译该程序:

$> gcc -g -o test test.c

/tmp/ccyQ5blC.o: In function `main':
/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open'
/home/tonybai/tmp/test.c:33: undefined reference to `libiconv'
/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close'

咦,这是咋搞的呢?怎么找不到iconv库的符号!!!显式加上iconv的链接指示再试试。

$> gcc -g -o test test.c -liconv

这回编译OK了。的确如那位同事所说出现了怪异的情况。

二、现场取证

惯性思维让我首先提出疑问:难道是这台机器上的libc版本有差异,检查一下libc中是否定义了iconv相关符号。

$ nm /lib64/libc.so.6 |grep iconv
000000397141e040 T iconv
000000397141e1e0 T iconv_close
000000397141ddc0 T iconv_open

iconv的函数都定义了呀!怎么会链接不到?

我们再来看看已经编译成功的那个test到底连接到哪个iconv库了。

$ ldd test
    linux-vdso.so.1 =>  (0x00007fff77d6b000)
    libiconv.so.2 => /usr/local/lib/libiconv.so.2 (0x00002abbeb09e000)
    libc.so.6 => /lib64/libc.so.6 (0×0000003971400000)
    /lib64/ld-linux-x86-64.so.2 (0×0000003971000000)

哦,系统里居然在/usr/local/lib下面单独安装了一份libiconv。gcc显然是链接到这里的libiconv了,但gcc怎么会链接到这里了呢?

三、大侦探的分析^_^

Gcc到底做了什么呢?我们看看其verbose的输出结果。

$ gcc -g -o test test.c -liconv -v
使用内建 specs。
目标:x86_64-redhat-linux
配置为:../configure –prefix=/usr –mandir=/usr/share/man –infodir=/usr/share/info –enable-shared –enable-threads=posix –enable-          checking=release –with-system-zlib –enable-__cxa_atexit –disable-libunwind-exceptions –enable-libgcj-multifile –enable-languages=c,c++,   objc,obj-c++,java,fortran,ada –enable-java-awt=gtk –disable-dssi –disable-plugin –with-java-home=/usr/lib/jvm/java-1.4.2-gcj-1.4.2.0/jre –with-cpu=generic –host=x86_64-redhat-linux
线程模型:posix
gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-50)
 /usr/libexec/gcc/x86_64-redhat-linux/4.1.2/cc1 -quiet -v test.c -quiet -dumpbase test.c -mtune=generic -auxbase test -g -version -o /tmp/     ccypZm0v.s
忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”
#include "…" 搜索从这里开始:
#include <…> 搜索从这里开始:
 /usr/local/include
 /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include
 /usr/include
搜索列表结束。
GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) (x86_64-redhat-linux)
    由 GNU C 版本 4.1.2 20080704 (Red Hat 4.1.2-50) 编译。
GGC 准则:–param ggc-min-expand=100 –param ggc-min-heapsize=131072
Compiler executable checksum: ef754737661c9c384f73674bd4e06594
 as -V -Qy -o /tmp/ccaqvDgX.o /tmp/ccypZm0v.s
GNU assembler version 2.17.50.0.6-14.el5 (x86_64-redhat-linux) using BFD version 2.17.50.0.6-14.el5 20061020
 /usr/libexec/gcc/x86_64-redhat-linux/4.1.2/collect2 –eh-frame-hdr -m elf_x86_64 –hash-style=gnu -dynamic-linker /lib64/ld-linux-x86-64.so.  2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crti.o /usr/   lib/gcc/x86_64-redhat-linux/4.1.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.1.2 -L/usr/lib/gcc/ x86_64-redhat-linux/4.1.2/../../../../lib64 -L/lib/../lib64
-L/usr/lib/../lib64 /tmp/ccaqvDgX.o -liconv -lgcc –as-needed -lgcc_s –no-as-needed -lc -lgcc –as-needed -lgcc_s –no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.1.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../lib64/crtn.o

从这个结果来看,gcc在search iconv.h这个头文件时,首先找到的是/usr/local/include/iconv.h,而不是/usr/include/iconv.h。这两个文件有啥不同么?

在/usr/local/include/iconv.h中,我找到如下代码:


#ifndef LIBICONV_PLUG
#define iconv_open libiconv_open
#endif
extern iconv_t iconv_open (const char* tocode, const char* fromcode);

libiconv_open vs iconv_open,卧槽!!!再对比一下前面编译时输出的错误信息:

/tmp/ccyQ5blC.o: In function `main':
/home/tonybai/tmp/test.c:28: undefined reference to `libiconv_open'
/home/tonybai/tmp/test.c:33: undefined reference to `libiconv'
/home/tonybai/tmp/test.c:38: undefined reference to `libiconv_close'

大侦探醒悟了!大侦探带你还原一下真实情况。

我们在执行gcc -g -o test test.c时, 根据gcc -v中include search dir的顺序,gcc首先search到的是/usr/local/include/iconv.h,而这里iconv_open等函数被预编译器替换成 了libiconv_open等加上了lib前缀的函数,而这些函数符号显然在libc中是无法找到的,libc中只有不带lib前缀的 iconv_open等函数的定义。大侦探也是一时眼拙了,没有细致查看gcc的编译错误信息中的内容,这就是问题所在!

gcc -g -o test test.c -liconv为何可以顺利编译通过呢?gcc是如何找到/usr/local/lib下的libiconv的呢?大侦探再次为大家还原一下真相。

我们在执行gcc -g -o test test.c -liconv时,gcc同 样首先search到的是/usr/local/include/iconv.h,然后编译test.c源码,ok;接下来启动ld程序进行链接;ld找 到了libiconv,ld是怎么找到iconv的呢,libiconv在/usr/local/lib下,ld显然是到这个目录下search了。我们 通过执行下面命令可以知晓ld的默认搜索路径:

$> ld -verbose|grep SEARCH
SEARCH_DIR("/usr/x86_64-redhat-linux/lib64"); SEARCH_DIR("/usr/local/lib64"); SEARCH_DIR("/lib64"); SEARCH_DIR("/usr/lib64"); SEARCH_DIR("/usr/x86_64-redhat-linux/lib"); SEARCH_DIR("/usr/lib64"); SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/lib"); SEARCH_DIR("/usr/lib");

ld的默认search路径中有/usr/local/lib(我之前一直是以为/usr/local/lib不是gcc/ld的默认搜索路径的),因此找到libiconv就不足为奇了。

四、问题解决

我们不想显式的加上-liconv,那如何解决这个问题呢?我们是否可以强制gcc先找到/usr/include/iconv.h呢?我们先来做个试验。

$ gcc -g -o test test.c -liconv -I ~/include -v

忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”
#include "…" 搜索从这里开始:
#include <…> 搜索从这里开始:
 /home/tonybai/include
 /usr/local/include
 /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include
 /usr/include
搜索列表结束。

试验结果似乎让我们觉得可行,我们通过-I指定的路径被放在了第一的位置进行search。我们来尝试一下强制gcc先search /usr/include。

$ gcc -g -o test test.c -I ~/include -v

忽略不存在的目录“/usr/lib/gcc/x86_64-redhat-linux/4.1.2/../../../../x86_64-redhat-linux/include”
忽略重复的目录“/usr/include”
  因为它是一个重复了系统目录的非系统目录
#include "…" 搜索从这里开始:
#include <…> 搜索从这里开始:
 /usr/local/include
 /usr/lib/gcc/x86_64-redhat-linux/4.1.2/include
 /usr/include
搜索列表结束。

糟糕!/usr/include被忽略了!还是从/usr/local/include开始,方案失败。

似乎剩下的唯一方案就是将/usr/local/lib下的那份libiconv卸载掉!那就这么做吧^_^!

C,C++开源项目中的100个Bugs

俄罗斯OOO Program Verification Systems公司用自己的静态源码分析产品PVS-Studio对一些知名的C/C++开源项目,诸如Apache Http ServerChromiumClangCMakeMySQL等的源码进行了分析,找出了100个典型的Bugs。个人觉得这份列表对C/C++ 程序员有一定参考意义。与其说事后用静态工具分析,倒不如在编码时就提高自知自觉,避免这份列表上的错误发生在你的代码中,因此这里将部分摘录一些Bugs(Bug编号这里不连续,为的是对应原文的编号)并做简要说明。原文将这份Bug列表分为了几类,这里也将沿用这个思路。

一、数组和字符串处理错误

数组和字符串处理错误是C/C++程序中最多的一类缺陷类型。这也可以看作是我们为拥有高效地底层内存操作能力而付出的代价。

[#1] Wolfenstein 3D项目 -"只有部分对象被clear了"

void CG_RegisterItemVisuals( int itemNum ) {
    …
    itemInfo_t *itemInfo;
    …
    memset( itemInfo, 0, sizeof( &itemInfo ) );
    …
}

这里的Bug出现在memset那一行。代码的真实意图是clear iteminfo这块内存,但调用memset时,第三个参数传入的却是sizeof(&iteminfo),要知道 sizeof(&itemInfo) != sizeof(itemInfo_t),前者只是一个指针的大小罢了。正确的写法是:

memset(itemInfo, 0, sizeof(itemInfo_t)); 或memset(itemInfo, 0, sizeof(*itemInfo));

[#2] Wolfenstein 3D项目 -"只有部分Matrix被clear了"

ID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {
    memcpy( mat, src, sizeof( src ) );
}

这里的Bug出现在memcpy一行。程序的原意是将clear src[3][3]这个二维数组。但这里有个坑:那就是作为函数形式参数的数组名已经退化为指针了,对其sizeof只能得到一个指针的长度,因此这里的 memcpy只是copy了一个指针的长度,没有copy全。这里的代码是C++代码,原文中给出了正确的改正方法 – 传reference:

ID_INLINE mat3_t::mat3_t( float (&src)[3][3] )
{
    memcpy( mat, src, sizeof( src ) );
}

[#4] ReactOS项目 – "错误地计算一个字符串的长度"

static const PCHAR Nv11Board = "NV11 (GeForce2) Board";
static const PCHAR Nv11Chip = "Chip Rev B2";
static const PCHAR Nv11Vendor = "NVidia Corporation";

BOOLEAN
IsVesaBiosOk(…)
{
    …
    if (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) &&
            !(strncmp(Product, Nv11Board, sizeof(Nv11Board))) &&
            !(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) &&
            (OemRevision == 0×311))
    …
}

Bug处在IsVesaBiosOK中那一串strncmp调用中,代码将一个指针的size传入strncmp作为第三个参数,导致 strncmp实际只是比较了字符串的前4 or 8个字节,而不是字符串的全部内容。

[#6] CPU Identifying Tool项目 – 数组越界

#define FINDBUFFLEN 64  // Max buffer find/replace size

int WINAPI Sticky (…)
{
    …
    static char findWhat[FINDBUFFLEN] = {'\0'};
    …
    findWhat[FINDBUFFLEN] = '\0';
    …
}

bug出在"findWhat[FINDBUFFLEN] = ‘\0′;”这一行。数组的最大长度为FINDBUFFLEN,但下标的最大值应该是FINDBUFFLEN-1,而不是FINDBUFFLEN。因此这 行代码显然应该改为findWhat[FINDBUFFLEN-1] = '\0';

[#7] Wolfenstein 3D项目 – 数组越界

typedef struct bot_state_s
{
    …
    char teamleader[32]; //netname of the team leader
    …
}  bot_state_t;

void BotTeamAI( bot_state_t *bs ) {
    …
    bs->teamleader[sizeof( bs->teamleader )] = '\0';
    …
}

"sizeof( bs->teamleader )]"这行的结果值已经超出了数组的最大边界,正确的代码是:

bs->teamleader[
  sizeof(bs->teamleader) / sizeof(bs->teamleader[0]) – 1
  ] = '\0';

[#8] Miranda IM项目 – 只Copy了部分字符串

struct _textrangew
{
    CHARRANGE chrg;
    LPWSTR lpstrText;
} TEXTRANGEW;

const wchar_t* Utils::extractURLFromRichEdit(…)
{
    …
    ::CopyMemory(tr.lpstrText, L"mailto:", 7);
    …
}

这里的bug在于L"mailto:"是宽字符串,宽字符串中的每个字符占2或4个字节(依Compiler使用的字符集编码而定),因此这里只 copy 7个字节显然是不够的,应该是7 * sizeof(wchar_t)。

[#9] CMake项目 – 循环內的数组越界

static const struct {
    DWORD   winerr;
    int     doserr;
} doserrors[] =
{
    …
};

static void
la_dosmaperr(unsigned long e)
{
    …
    for (i = 0; i < sizeof(doserrors); i++)
    {
        if (doserrors[i].winerr == e)
        {
            errno = doserrors[i].doserr;
            return;
        }
    }
    …
}

作者原本意图la_dosmaperr中for循环的次数等于数组的元素个数,但sizeof(doserrors)返回的却是数组占用的字节个数,这远远大于数组元素个数,因此造成数组越界。正确的写法:

for (i = 0; i < sizeof(doserrors) / sizeof(*doserrors); i++)

[#10] CPU Identifying Tool项目 – 打印到自身的字符串

char * OSDetection ()
{
    …
    sprintf(szOperatingSystem,
                    "%sversion %d.%d %s (Build %d)",
                    szOperatingSystem,
                    osvi.dwMajorVersion,
                    osvi.dwMinorVersion,
                    osvi.szCSDVersion,
                    osvi.dwBuildNumber & 0xFFFF);
    …
    sprintf (szOperatingSystem, "%s%s(Build %d)",
                      szOperatingSystem, osvi.szCSDVersion,
                      osvi.dwBuildNumber & 0xFFFF);
    …
}

通过sprintf,szOperatingSystem字符串将自己打印到自己里面,这是十分危险的,将导致无法预知的错误结果,可能会导致栈溢出等严重问题。

[#12] Notepad++项目 – 数组局部clear

#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];

DockingManager::DockingManager()
{
    …
    memset(_iContMap, -1, CONT_MAP_MAX);
    …
}

代码的原本试图将数组_iContMap清零,但memset的第三个参数CONT_MAP_MAX并不能代表数组的真正大小,而只是数组的元素个数而已,显然其忘记乘以sizeof(int)了。

二、未定义行为

在C/C++的语言规范中,我们常常能看到“xx is undefined”。规范中并没有明确表明这类错误是什么样子的,只是说取决于Compiler的实现,也许Compiler会给出正确的结果,但这么使用却是不可移植的。

[#1] Chromium项目 – 智能指针的误用

void AccessibleContainsAccessible(…)
{
    …
    auto_ptr<VARIANT> child_array(new VARIANT[child_count]);
    …
}

这里的问题在于使用new[]分配的内存,在智能指针释放时却用了delete,这将会导致未定义行为。看看autoptr的destructor就知道了:

~auto_ptr() {
    delete _Myptr;
}

我们可以找一些更合适的类来fix这个问题,比如boost::scopedarray。

[#2] IPP Sample项目 – 经典未定义行为

template<typename T, Ipp32s size> void HadamardFwdFast(…)
{
  Ipp32s *pTemp;
  …
  for(j=0;j<4;j++) {
    a[0] = pTemp[0*4] + pTemp[1*4];
    a[1] = pTemp[0*4] – pTemp[1*4];
    a[2] = pTemp[2*4] + pTemp[3*4];
    a[3] = pTemp[2*4] – pTemp[3*4];
    pTemp = pTemp++;
    …
  }
  …
}

很多人一眼就看到了"pTemp = pTemp++"这行,对于这个代码编译器会产生两种结果截然不同的翻译:

pTemp = pTemp + 1;
pTemp = pTemp;

TMP = pTemp;
pTemp = pTemp + 1;
pTemp = TMP;

到底是哪种呢?依赖于编译器的实现,甚至是优化级别的设定。

三、与运算优先级相关的错误

[#1] MySQL工程 – !和&的运算优先级

int ha_innobase::create(…)
{
  …
  if (srv_file_per_table
            && !mysqld_embedded
            && (!create_info->options & HA_LEX_CREATE_TMP_TABLE)) {
  …
}

这段代码原意是想测试create_info->options变量中几个bit位的值是否set了,即!(create_info->options & HA_LEX_CREATE_TMP_TABLE),但由于!的运算优先级高于&,实际逻辑变成了(!create_info->options) & HA_LEX_CREATE_TMP_TABLE了。如果想要这段代码如期工作,就不要吝啬小括号了。

[#2] Emule工程 – *和++的运算优先级

STDMETHODIMP
CCustomAutoComplete::Next(…, ULONG *pceltFetched)
{
  …
  if (pceltFetched != NULL)
    *pceltFetched++;
  …
}

显然作者原意是想对pceltFetched所指向的long型变量进行++操作,但由于*和++的运算优先级没有搞对,导致实际上执行了*(pceltFetched++)的操作,而不是(*pceltFetched)++操作。

[#3] Chromium项目 – &和!=的运算优先级

#define FILE_ATTRIBUTE_DIRECTORY 0×00000010

bool GetPlatformFileInfo(PlatformFile file, PlatformFileInfo* info) {
  …
  info->is_directory =
    file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0;
  …
}

这个程序员的意图是通过测试file_info.dwFileAttributes的几个bit位的值来判定是否是目录,逻辑上应该是(file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0,但由于!=优先级高于&,原代码中无括号,结果逻辑变成了file_info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY != 0),导致is_directory将永远求值为true。

[#4] BCmenu项目 – if和else弄混

void BCMenu::InsertSpaces(void)
{
  if(IsLunaMenuStyle())
    if(!xp_space_accelerators) return;
  else
    if(!original_space_accelerators) return;
  …
}

这又是C语言的一个“大坑”,无奈这个BCMenu项目的程序员掉坑里了。虽然从代码缩进上来看,else似乎是与最外层的if配对使用,但实际这段代码的效果是:

if(IsLunaMenuStyle())
{
   if(!xp_space_accelerators) {
     return;
   } else {
     if(!original_space_accelerators) return;
   }
}

这显然不是程序员原意,看来括号必要时还是不能省略的。修改后的代码如下:

if(IsLunaMenuStyle()) {
  if(!xp_space_accelerators) return;
} else {
  if(!original_space_accelerators) return;
}

四、格式化输出错误

[#1] ReactOS项目 – 错误地输出WCHAR字符

static void REGPROC_unescape_string(WCHAR* str)
{
  …
  default:
    fprintf(stderr,
            "Warning! Unrecognized escape sequence: \\%c'\n",
            str[str_idx]);
  …
}

%c是用来格式化输出非宽字符的,这里用来输出WCHAR显然会得到错误的结果,fix solution是将%c换位%C。

[#2] Intel AMT SDK项目 – 缺少%s

void addAttribute(…)
{
  …
  int index = _snprintf(temp, 1023, 
                        "%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
                        "%02x%02x:02x%02x:%02x%02x:%02x%02x",
                        value[0],value[1],value[2],value[3],value[4],
                        value[5],value[6],value[7],value[8],
                        value[9],value[10],value[11],value[12],
                        value[13],value[14],value[15]);
  …
}

 

不解释了,自己慢慢数和对照吧。

[#3] Intel AMT SDK项目 – 未使用的参数

bool GetUserValues(…)
{
  …
  printf("Error: illegal value. Aborting.\n", tmp);
  return false;
}

显然tmp是多余的。

五、书写错误

[#1] Miranda IM项目 – 在if中赋值

void CIcqProto::handleUserOffline(BYTE *buf, WORD wLen)
{
  …
  else if (wTLVType = 0×29 && wTLVLen == sizeof(DWORD))
  …
}

“wTLVType = 0×29”显然是笔误,应该是“wTLVType == 0×29”才对。

[#3] Clang项目 – 对象名书写错误

static Value *SimplifyICmpInst(…) {
  …
  case Instruction::Shl: {
    bool NUW =
      LBO->hasNoUnsignedWrap() && LBO->hasNoUnsignedWrap();
    bool NSW =
      LBO->hasNoSignedWrap() && RBO->hasNoSignedWrap();
  …
}

从最后一行先后使用了LBO和RBO来看,前面只用了LBO的那行很可能是有问题的,正确的应该是:

bool NUW =
      LBO->hasNoUnsignedWrap() && RBO->hasNoUnsignedWrap();

[#6] G3D Content Pak项目 – 一对括号放错了地方

bool Matrix4::operator==(const Matrix4& other) const {
  if (memcmp(this, &other, sizeof(Matrix4) == 0)) {
    return true;
  }
  …
}

由于括号放错了地方,导致memcmp最后的参数变成了sizeof(Matrix4) == 0,这行代码的正确写法应该是:

if (memcmp(this, &other, sizeof(Matrix4)) == 0) {

[#8] Apache Http Server项目 – 多余的sizeof

PSECURITY_ATTRIBUTES GetNullACL(void)
{
  PSECURITY_ATTRIBUTES sa;
  sa  = (PSECURITY_ATTRIBUTES)
    LocalAlloc(LPTR, sizeof(SECURITY_ATTRIBUTES));
  sa->nLength = sizeof(sizeof(SECURITY_ATTRIBUTES));
  …
}

最后一行显然是笔误,sizeof(sizeof(SECURITY_ATTRIBUTES))应该写为sizeof(SECURITY_ATTRIBUTES)才对。

[#10] Notepad++项目 – 在本来应该用&的地方使用了&&

TCHAR GetASCII(WPARAM wParam, LPARAM lParam)
{
  …
  result=ToAscii(wParam,
                 (lParam >> 16) && 0xff, keys,&dwReturnedValue,0);
  …
}

(lParam >> 16) && 0xff没有什么意义,求值结果总是true。这里的代码应该是(lParam >> 16) & 0xff。

[#12] Fennec Media Project项目 – 额外的分号

int settings_default(void)
{
  …
  for(i=0; i<16; i++);
    for(j=0; j<32; j++)
    {
      settings.conversion.equalizer_bands.boost[i][j] = 0.0;
      settings.conversion.equalizer_bands.preamp[i]   = 0.0;
    }
}

这又是一个实际逻辑与代码缩进不符的例子。作者的原意是这样的:

for(i=0; i<16; i++) 
{
    for(j=0; j<32; j++)
    {
      settings.conversion.equalizer_bands.boost[i][j] = 0.0;
      settings.conversion.equalizer_bands.preamp[i]   = 0.0;
    }
}

但实际执行代码逻辑却是:

for(i=0; i<16; i++) 
{
    ;
}

for(j=0; j<32; j++)
{   
  settings.conversion.equalizer_bands.boost[i][j] = 0.0;
  settings.conversion.equalizer_bands.preamp[i]   = 0.0;
}

这一切都是那个;导致的。

六、对基本函数和类的误用

[#2] TortoiseSVN项目 – remove函数的误用

STDMETHODIMP CShellExt::Initialize(….)
{
  …
  ignoredprops = UTF8ToWide(st.c_str());
  // remove all escape chars ('\\')
  std::remove(ignoredprops.begin(), ignoredprops.end(), '\\');
  break;
  …
}

作者意图删除所有'\\',但他用错了函数,remove函数只是交换元素的位置,将要删除的元素交换到尾部trash,并且返回指向trash首地址的iterator。正确的做法应该是"v.erase(remove(v.begin(), v.end(), 2), v.end())"。

[#5] Pixie项目 – 在循环中使用alloca函数

inline  void  triangulatePolygon(…) {
  …
  for (i=1;i<nloops;i++) {
    …
    do {
      …
      do {
        …
        CTriVertex  *snVertex =
         (CTriVertex *)alloca(2*sizeof(CTriVertex));
        …
      } while(dVertex != loops[0]);
      …
    } while(sVertex != loops[i]);
    …
  }
  …
}

alloca函数在栈上分配内存,因此在循环中使用alloca可能会很快导致栈溢出。

七、无意义的代码

[#1] IPP Samples项目 – 不完整的条件

void lNormalizeVector_32f_P3IM(Ipp32f *vec[3],
                                 Ipp32s* mask, Ipp32s len)
{
  Ipp32s  i;
  Ipp32f  norm;

  for(i=0; i<len; i++) {
    if(mask<0) continue;
    norm = 1.0f/sqrt(vec[0][i]*vec[0][i]+
                     vec[1][i]*vec[1][i]+vec[2][i]*vec[2][i]);
    vec[0][i] *= norm; vec[1][i] *= norm; vec[2][i] *= norm;
  }
}

mask是Ipp32s类型指针,这样if (mask< 0)这句代码显然没啥意义,正确的代码应该是:

if (mask[i] < 0) continue;

[#2] QT项目 – 重复的检查

Q3TextCustomItem* Q3TextDocument::parseTable(…)
{
  …
  while (end < length
         && !hasPrefix(doc, length, end, QLatin1String("</td"))
         && !hasPrefix(doc, length, end, QLatin1String("<td"))
         && !hasPrefix(doc, length, end, QLatin1String("</th"))
         && !hasPrefix(doc, length, end, QLatin1String("<th"))
         && !hasPrefix(doc, length, end, QLatin1String("<td"))
         && !hasPrefix(doc, length, end, QLatin1String("</tr"))
         && !hasPrefix(doc, length, end, QLatin1String("<tr"))
         && !hasPrefix(doc, length, end, QLatin1String("</table"))) {

  …
}

这里对"<td"做了两次check。

八、总是True或False的条件

[#1] Shareaza项目 – char类型的值范围

void CRemote::Output(LPCTSTR pszName)
{

  …
  CHAR* pBytes = new CHAR[ nBytes ];
  hFile.Read( pBytes, nBytes );
  …
  if ( nBytes > 3 && pBytes[0] == 0xEF &&
             pBytes[1] == 0xBB && pBytes[2] == 0xBF )
  {
    pBytes += 3;
    nBytes -= 3;
    bBOM = true;
  }
  …
}

表达式"pBytes[0] == 0xEF"总是False。char类型的值范围是-128~127 < 0xEF,因此这个表达式总是False,导致整个if condition总是为False,与预期逻辑不符。

[#3] VirtualDub项目 – 无符号类型总是>=0

typedef unsigned short wint_t;

void lexungetc(wint_t c) {
  if (c < 0)
    return;
   g_backstack.push_back(c);
}

c是unsigned short类型,永远不会小于0,也就是说if (c < 0)永远为False。

[#8] MySQL项目 – 条件错误

enum enum_mysql_timestamp_type
str_to_datetime(…)
{
  …
  else if (str[0] != ‘a’ || str[0] != 'A')
    continue; /* Not AM/PM */
  …
}

if (str[0] != ‘a’ || str[0] != 'A')这个条件永远为真。也许这块本意是想用&&。

九、代码漏洞

导致漏洞的代码错误实际上也都是笔误、不正确的条件以及不正确的数组操作等。但这里还是想将一些特定错误划归为一类,因为入侵者可以利用这些错误来攻击你的代码,获取其利益。

[#1] Ultimate TCP/IP项目 – 空字符串的错误检查

char *CUT_CramMd5::GetClientResponse(LPCSTR ServerChallenge)
{
  …
  if (m_szPassword != NULL)
  {
    …
    if (m_szPassword != '\0')
    {
  …
}

第二个if condition check意图检查m_szPassword是否为空字符串,但却错误的将指针与'\0'进行比较,正确的代码应该是这样的:

if (*m_szPassword != '\0')

[#2] Chromium项目 – NULL指针的处理

bool ChromeFrameNPAPI::Invoke(…)
{
  ChromeFrameNPAPI* plugin_instance =
    ChromeFrameInstanceFromNPObject(header);
  if (!plugin_instance &&
      (plugin_instance->automation_client_.get()))
    return false;
  …
}   

一旦plugin_instance为NULL,!plugin_instance为True,代码对&&后面的子条件求值,引用plugin_instance将导致程序崩溃。正确的做法应该是:

if (plugin_instance &&
        (plugin_instance->automation_client_.get()))
  return false;

[#5] Apache httpd Server项目 – 不完整的缓冲区clear

#define MEMSET_BZERO(p,l)       memset((p), 0, (l))

void apr__SHA256_Final(…, SHA256_CTX* context) {
  …
  MEMSET_BZERO(context, sizeof(context));
  …
}

这个错误前面提到过,sizeof(context)只是指针的大小,将之改为sizeof(*context)就OK了。

[#7] PNG Library项目 – 意外的指针clear

png_size_t
png_check_keyword(png_structp png_ptr, png_charp key,
                    png_charpp new_key)
{
  …
  if (key_len > 79)
  {
    png_warning(png_ptr, "keyword length must be 1 – 79 characters");
    new_key[79] = '\0';
    key_len = 79;
  }
  …
}

new_key的类型为png_charpp,顾名思义,这是一个char**类型,但代码中new_key[79] = ‘\0′这句显然是要给某个char赋值,但new_key[n]得到的应该是一个地址,给一个地址赋值为’\0′显然是有误的。正确的写法应该是(*new_key)[79] = '\0'。

[#10] Miranda IM项目 – 保护没生效

void Append( PCXSTR pszSrc, int nLength )
{
  …
  UINT nOldLength = GetLength();
  if (nOldLength < 0)
  {
    // protects from underflow
    nOldLength = 0;
  }
  …
}

nOldLength椒UINT类型,其值永远不会小于0,因此if (nOldLength < 0)这行成了摆设。

[#12] Ultimate TCP/IP项目 – 不正确的循环结束条件

void CUT_StrMethods::RemoveSpaces(LPSTR szString) {
  …
  size_t loop, len = strlen(szString);
  // Remove the trailing spaces
  for(loop = (len-1); loop >= 0; loop–) {
    if(szString[loop] != ' ')
      break;
  }
  …
}

循环中的结束条件loop >= 0将永远为True,因为loop变量的类型是size_t是unsigned类型,永远不会小于0。

十、拷贝粘贴

和笔误不同,程序员们决不因该低估拷贝粘贴问题,这类问题发生了太多。程序员们花费了大量时间在这些问题的debug上。

[#1] Fennec Media Project项目 – 处理数组元素时出错

void* tag_write_setframe(char *tmem,
                         const char *tid, const string dstr)
{
  …
  if(lset)
  {
    fhead[11] = '\0';
    fhead[12] = '\0';
    fhead[13] = '\0';
    fhead[13] = '\0';
  }
  …
}

 

咋看一下,fhead[13]做了两次赋值,似乎没啥问题。但仔细想一下,最后那行程序员的原意极可能是想写fhead[14] = '\0'。问题就在这里了。

[#2] MySQL项目 – 处理数组元素时出错

static int rr_cmp(uchar *a,uchar *b)
{
  if (a[0] != b[0])
    return (int) a[0] – (int) b[0];
  if (a[1] != b[1])
    return (int) a[1] – (int) b[1];
  if (a[2] != b[2])
    return (int) a[2] – (int) b[2];
  if (a[3] != b[3])
    return (int) a[3] – (int) b[3];
  if (a[4] != b[4])
    return (int) a[4] – (int) b[4];
  if (a[5] != b[5])
    return (int) a[1] – (int) b[5];
  if (a[6] != b[6])
    return (int) a[6] – (int) b[6];
  return (int) a[7] – (int) b[7];
}

 

编写这类代码时,我猜绝大多数人会选择Copy-Paste,然后再逐行修改,问题就发生在修改过程中,上面的代码中当处理a[5] != b[5]时就忘记修改一个下标了:return (int) a[1] – (int) b[5];显然这里的正确代码应该是return (int) a[5] – (int) b[5]。

[#3] TortoiseSVN项目 文件名不正确

BOOL GetImageHlpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,               
                                dwLS)) ;            
}

BOOL GetDbgHelpVersion(DWORD &dwMS, DWORD &dwLS)
{
  return(GetInMemoryFileVersion(("DBGHELP.DLL"),
                                dwMS,                           
                                dwLS)) ;                        
}

GetImageHlpVersion和GetDbgHelpVersion都使用了"DBGHELP.DLL"文件,显然GetImageHlpVersion写错文件名了。应该用"IMAGEHLP.DLL"就对了。

[#4] Clang项目 – 等同的函数体

MapTy PerPtrTopDown;
MapTy PerPtrBottomUp;

void clearBottomUpPointers() {
  PerPtrTopDown.clear();
}

void clearTopDownPointers() {
  PerPtrTopDown.clear();
}

我们看到虽然两个函数名不同,但是函数体的内容是相同的,显然又是copy-paste惹的祸。做如下修改即可:

void clearBottomUpPointers() {
  PerPtrBottomUp.clear();
}

 

十一、Null指针的校验迟了

这里的“迟了”的含义是先使用指针,然后再校验指针是否为NULL。

[#1] Quake-III-Arena项目 – 校验迟了

void Item_Paint(itemDef_t *item) {
  vec4_t red;
  menuDef_t *parent = (menuDef_t*)item->parent;
  red[0] = red[3] = 1;
  red[1] = red[2] = 0;
  if (item == NULL) {
    return;
  }
  …
}

 

在校验item是否为NULL前已经使用过item了,一旦item真的为NULL,那程序必然崩溃。

十二、其他杂项

[#1] Image Processing 项目 – 八进制数

inline
void elxLuminocity(const PixelRGBus& iPixel,
                     LuminanceCell< PixelRGBus >& oCell)
{
  oCell._luminance = uint16(0.2220f*iPixel._red +
                            0.7067f*iPixel._blue + 0.0713f*iPixel._green);
  oCell._pixel = iPixel;
}

inline
void elxLuminocity(const PixelRGBi& iPixel,
                     LuminanceCell< PixelRGBi >& oCell)
{
  oCell._luminance = 2220*iPixel._red +
    7067*iPixel._blue + 0713*iPixel._green;
  oCell._pixel = iPixel;
}

第二个函数,程序员原意是使用713这个十进制整数,但0713 != 713,在C中,0713是八进制的表示法,Compiler会认为这是个八进制数。

[#2] IPP Sample工程 – 一个变量用于两个loop中

JERRCODE CJPEGDecoder::DecodeScanBaselineNI(void)
{
  …
  for(c = 0; c < m_scan_ncomps; c++)
  {
    block = m_block_buffer + (DCTSIZE2*m_nblock*(j+(i*m_numxMCU)));

    // skip any relevant components
    for(c = 0; c < m_ccomp[m_curr_comp_no].m_comp_no; c++)
    {
      block += (DCTSIZE2*m_ccomp[c][/c][/c].m_nblocks);
    }
  …
}

变量c用在了两个loop中,这会导致只有部分数据被处理,或外部循环中止。

[#3] Notepad++项目 – 怪异的条件表达式

int Notepad_plus::getHtmlXmlEncoding(….) const
{
  …
  if (langT != L_XML && langT != L_HTML && langT == L_PHP)
    return -1;
  …
}

代码中的那行if条件等价于 if (langT == L_PHP),显然似乎不是作者原意,猜测正确的代码应该是这样的:

int Notepad_plus::getHtmlXmlEncoding(….) const
{
  …
  if (langT != L_XML && langT != L_HTML && langT != L_PHP)
    return -1;
  …
}

Hello,Sublime Text 2

用惯了Vim后,也会有一种尝试新Editor的冲动,这回Sublime Text 2满足了我的这个需求。据说Sublime Text是目前最火的代码编辑器之一,我周围为数不多的几个比较Geek的同事都已经开始使用Sublime Text 2或用了很长时间了,其官方网站首页的Feature Demo也的确非常地炫。

安装Sublime Text 2

我的实验环境Ubuntu 12.04.1 32-bit Desktop版,默认Ubuntu Unity桌面,iBus拼音输入法。

Sublime Text 2的安装极其简单,遵循着download(http://www.sublimetext.com/2) -> unzip -> add path -> start and use的经典路线。我下载的Sublime Text 2是2.0.1版本,启动后一切正常。

安装后目录结构

安装后的Sublime Text 2的目录结构非常简洁:

$ ls
Icon/  PackageSetup.py   Pristine Packages/
lib/   sublime_plugin.py   sublime_text*

lib下是自带的Python26环境;Pristine Packages下是各种编程语言的插件包。

在我的环境下Sublime Text 2的用户配置与包环境放在了~/.config/sublime-text-2/下面,

$ ls
Installed Packages/  Packages/  Pristine Packages/  Settings/

这里面最重要的目录就是Packages目录了,这里是Sublime Text 2用第三方包扩展自身Feature的包存储路径。

安装package control

package control包之于Sublime Text 2就好比apt工具之于Ubuntu,它是一个方便第三方包安装、卸载和管理的第三方包。在其官网(http://wbond.net/sublime_packages/package_control)上明示了其安装方法:

* 敲入 ctrl + ` 调出命令行窗口
* 在命令行窗口中输入下面的代码,回车执行。

import urllib2,os; pf='Package Control.sublime-package'; ipp=sublime.installed_packages_path(); os.        makedirs(ipp) if not os.path.exists(ipp) else None; urllib2.install_opener(urllib2.build_opener(urllib2.   ProxyHandler())); open(os.path.join(ipp,pf),'wb').write(urllib2.urlopen('http://sublime.wbond.net/'+pf.replace(' ','%20')).read()); print('Please restart Sublime Text to finish installation')

* 重启Sublime Text 2。

注意:如果需要代理访问外网的话,需要正确设置http_proxy环境变量。

敲入"ctrl + shift + p"可打开命令窗口,输入"Package Control",你会看到窗口下拉提示中Package Control支持的功能,常用的我们会选择:“Package Control: Install Package”。

安装中文支持

中国程序员每每在尝试一种国外程序员新开发的编辑器时,都会遇到中文字符集编码的问题,这次Sublime Text 2也不例外,它原生就不支持中文显示。还好中国程序员是无比聪明的,开发了ConvertToUTF8这样的第三方包,让我们可以看到中文并用中文编辑。

最简单的安装ConvertToUTF8的方法就是用Package Control安装,选择Package Control: Install Package后,搜素ConvertToUTF8,找到后,点击即可安装。安装后,你会在~/.config/sublime-text-2/Packages下面看到ConvertToUTF8包目录。

再次启动Sublime Text 2后,打开一个GBK编码的中文文档,居然提示ConvertToUTF8工作不正常。后发现ConvertToUTF8主页上有提示,Python 2.6下的ConvertToUTF8需要一个Codecs26的Package才能正常运行。下载Codecs26后,解压安装到Packages下面,重新启动Sublime Text 2,Sublime Text 2直接dump core。从Packages目录下将Codecs26删除后,Sublime Text 2恢复正常。

又细致读了ConvertToUTF8作者的README文件,发现master branch上的Codecs26是for 64位版本的,我需要下载x32 branch上的包。的确,下载并安装x32 branch上的Codecs26后,Sublime Text 2启动OK,转换中文OK了。

注意:不要与其他支持GBK转换的包(比如GBK Encoding Support)混用,否则ConvertToUTF8无法works。

解决中文输入问题

好不容易能看GBK编码的中文文件了,却发现无法输入中文,无论如何切换输入法和重启输入法,都无法输入中文。网上介绍可通过"Input Helper Package(cd .config/sublime-text-2/Packages; git clone http://github.com/xgenvn/InputHelper.git)"解决问题。问题的确可以解决,不过输入中文时太麻烦了:需要先敲入"ctrl+shift+z"调出中文输入框,再在这个框里输入中文。

网上都说这是iBus输入法与Sublime Text 2的兼容问题,要想解决就要换fcitx。以前用过fcitx感觉默认输入法比较弱,不过现在fcitx有google pinyin了,体验一定会提高不少。通过下面命令一键安装fcitx:

sudo apt-get install fcitx fcitx-googlepinyin

安装后,在“语言支持”中用fcitx替换掉iBus。在“启动应用程序”中加入:

名称: Fcitx
命令: /usr/bin/fcitx -d
注释t: Fcitx启动

注销再登录后,再打开Sublime Text 2,终于可以输入中文了。

功能

用了一遭儿,Sublime Text 2最吸引我的Feature包括:“Goto Anything”和“Multi-Selection”。在一个工程中,通过ctrl + p调出一个输入框,Sublime Text 2首先在文件名级别对你输入的文本进行匹配;待选择好文件后,继续输入@,可看到下拉列表中显示这个文件中所有函数名的名称列表;如果输入的是#,那么下拉列表中将显示该文件中的所有符号。选择某个函数名或符号后,光标将停留在某个符号上,这时我们可以用Multi-Selection这个功能了,如果你要将这个文件中同名符号全选出来,直接Alt+F3即可;如果要选择接下来的N个同名符号,那么敲入N次ctrl + D即可。

不过要想实现ctags那种在符号上跳转到符号定义或符号调用者的功能,Sublime Text 2还无法原生支持,可考虑安装Sublime Text 2的Ctags插件实现:直接在Packages目录下git clone https://github.com/SublimeText/CTags.git。之后:
- “ctrl +t, ctrl+ r"会重新生成tags文件(前提:系统内安装了ctags程序)
- "ctrl +t, ctrl + t"会跳到光标所在符号的定义处;
- "ctrl + t, ctrl + b"会跳回上次的位置;

感受

Sublime Text 2给我的最大感受就是“快”!你在搜索、切换符号、选择文件列表中文件或符号的同时,整个文件会同步的展现你的屏幕上。




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:


如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多