在Solaris上编译redis(2.8.13)

最近由于工作需要,又要做redis相关的工作。正好今天早上redis发布了最新的2.8.13版本,于是就下载了最新的版本并试着在Solaris上编译了一下,还是发现了一些问题。于是在此记录下来,说不定对其他人会有帮助。

(1)Solaris 9

我先选了一台Solaris 9,结果一开始编译就遇到“IPV6_V6ONLY”未定义错误。上网查了一下,发现Solaris不支持这个socket选项。我改了一点代码,绕过去这个问题,结果再编译遇到了没有stdint.h文件这个错误。于是果断决定放弃Solaris 9,转投Solaris 10。

(2)Solaris 10

Solaris 10上的编译过程还是比较顺利的,但是链接时出了两个问题:

a)nanosleep函数没有定义:Solaris上调用nanosleep,需要链接“rt library”,即在Makefile里加上“-lrt”。我把这个patch报给了redis(https://github.com/NanXiao/redis/commit/232d77938add77d95c91a714462389e62e2bf126),不过不确定redis会不会采纳。

b)isinf没有定义,需要在调用make命令时传入MISSING_ISINF宏定义,即“make “CFLAGS+=-DMISSING_ISINF” ”。

Source Insight的“Expand tabs”选项介绍

Source Insight有个“Expand tabs”选项(Options->Document Options->Editing Options->Expand tabs),其功能是把Tab展开成空格:比方说,你的Source Insight的设置是1个Tab占4个空格的宽度,那么当你勾选“Expand tabs”选项,代码里相应的一个Tab就变成四个空格了。这个选项在下面的情形会有用:

比如说Github上某个项目的缩进都是用4个空格,而Github的Tab默认又是8个空格。我本地的Source Insight设置又是一个Tab占4个空格的宽度。这样我本地代码采用Tab缩进,显示是对齐的(一个Tab占4个空格的宽度),但是check in Github上,就发现代码会多缩进(Github的Tab默认是8个空格)。这时如果我把“Expand tabs”选项选上,所有的Tab都变成了空格,这样check in以后就是4个空格,显示代码就是对齐的了。

如何写gdb命令脚本

作为UNIX/Linux下使用广泛的调试器,gdb不仅提供了丰富的命令,还引入了对脚本的支持:一种是对已存在的脚本语言支持,比如python,用户可以直接书写python脚本,由gdb调用python解释器执行;另一种是命令脚本(command file),用户可以在脚本中书写gdb已经提供的或者自定义的gdb命令,再由gdb执行。在这篇文章里,我会介绍一下如何写gdb的命令脚本。

(一) 自定义命令
gdb支持用户自定义命令,格式是:

define commandName  
    statement  
    ......  
end  

其中statement可以是任意gdb命令。此外自定义命令还支持最多10个输入参数:$arg0$arg1 …… $arg9,并且还用$argc来标明一共传入了多少参数。

下面结合一个简单的C程序(test.c),来介绍如何写自定义命令:

#include <stdio.h>

int global = 0;

int fun_1(void)
{
    return 1;
}

int fun_a(void)
{
    int a = 0;
    printf("%d\n", a);
}

int main(void)
{
    fun_a();
    return 0;
}

首先编译成可执行文件:

gcc -g -o test test.c

接着用gdb进行调试:

[root@linux:~]$ gdb test
GNU gdb (GDB) 7.6
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /data2/home/nanxiao/test...done.
(gdb) b fun_a
Breakpoint 1 at 0x4004d7: file test.c, line 12.
(gdb) r
Starting program: /data2/home/nanxiao/test

Breakpoint 1, fun_a () at test.c:12
12              int a = 0;
(gdb) bt
#0  fun_a () at test.c:12
#1  0x0000000000400500 in main () at test.c:18

可以看到使用btbacktrace缩写)命令可以打印当前线程的调用栈。我们的第一个自定义命令就是也实现一个backtrace功能:

define mybacktrace
    bt
end

怎么样?简单吧,纯粹复用gdb提供的命令。下面来验证一下:

(gdb) define mybacktrace
Type commands for definition of "mybacktrace".
End with a line saying just "end".
>bt
>end
(gdb) mybacktrace
#0  fun_a () at test.c:12
#1  0x0000000000400500 in main () at test.c:18

功能完全正确!

接下来定义一个赋值命令,把第二个参数的值赋给第一个参数:

define myassign
    set var $arg0 = $arg1
end

执行一下:

(gdb) define myassign
Type commands for definition of "myassign".
End with a line saying just "end".
>set var $arg0 = $arg1
>end
(gdb) myassign global 3
(gdb) p global
$1 = 3

可以看到global变量的值变成了3

对于自定义命令来说,传进来的参数只是进行简单的文本替换,所以你可以传入赋值的表达式,甚至是函数调用:

(gdb) myassign global fun_1()
(gdb) p global
$2 = 1

可以看到global变量的值变成了1

除此以外,还可以为自定义命令写帮助文档,也就是执行help命令时打印出的信息:

document myassign
    assign the second parameter value to the first parameter
end

执行help命令:

(gdb) document myassign
Type documentation for "myassign".
End with a line saying just "end".
>assign the second parameter value to the first parameter
>end
(gdb) help myassign
assign the second parameter value to the first parameter

可以看到打印出了myassign的帮助信息。

(二) 命令脚本
首先对于命令脚本的命名,其实gdb没有什么特殊要求,只要文件名不是gdb支持的其它脚本语言的文件名就可以了(比如.py)。因为这样做会使gdb按照相应的脚本语言去解析命令脚本,结果自然是不对的。

其次为了帮助用户写出功能强大的脚本,gdb提供了如下的流程控制命令:
(1)条件命令:if...else...end。这个同其它语言中提供的if命令没什么区别,只是注意结尾的end
(2)循环命令:while...end。gdb同样提供了loop_breakloop_continue命令分别对应其它语言中的breakcontinue,另外同样注意结尾的end

另外gdb还提供了很多输出命令。比方说echo命令,如果仅仅是输出一段文本,echo命令特别方便。此外还有和C语言很相似的支持格式化输出的printf命令,等等。

脚本文件的注释也是以#开头的,这个同很多其它脚本语言都一样。

最后指出的是在gdb中执行脚本要使用source命令,例如:“source xxx.gdb”。

(三) 一个完整的例子

最后以一个完整的gdb脚本(search_byte.gdb)做例子,来总结一下本文提到的内容:

define search_byte
    if $argc != 3
        help search_byte
    else
        set $begin_addr = $arg0
        set $end_addr = $arg1

        while $begin_addr <= $end_addr
            if *((unsigned char*)$begin_addr) == $arg2
                printf "Find it!The address is 0x%x\n", $begin_addr
                loop_break
            else
                set $begin_addr = $begin_addr + 1
            end
        end

        if $begin_addr > $end_addr
            printf "Can't find it!\n"
        end
    end
end

document search_byte
    search a specified byte value(0 ~ 255) during a memory
    usage: search_byte begin_addr end_addr byte
end

这个脚本定义了search_byte命令,目的是在一段指定内存查找一个值(unsigned char类型):需要输入内存的起始地址,结束地址和要找的值。
命令逻辑可以分成3个部分:
(a) 首先判断输入参数是不是3个,如果不是,就输出帮助信息;
(b) 从起始地址开始查找指定的值,如果找到,打印地址值并退出循环,否则把地址加1,继续查找;
(c) 如果在指定内存区域没有找到,打印提示信息。

另外这个脚本还定义了search_byte的帮助信息。

仍然以上面的C程序为例,来看一下如何使用这个gdb脚本:

[root@linux:~]$ gdb test
GNU gdb (GDB) 7.6
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-unknown-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /data2/home/nanxiao/test...done.
(gdb) p &global
$1 = (int *) 0x600900 <global>
(gdb) p global
$2 = 0
(gdb) source search_byte.gdb
(gdb) search_byte 0x600900 0x600903 0
Find it! The address is 0x600900
(gdb) search_byte 0x600900 0x600903 1
Can't find it!

可以看到global的值是0,起始地址是0x600900,结束地址是0x600903。在global的内存区域查找0成功,查找1失败。

受篇幅所限,本文只是对gdb命令脚本做了一个粗浅的介绍,旨在起到抛砖引玉的效果。如果大家想更深入地了解这部分知识,可以参考gdb手册的相关章节:Extending GDB (https://sourceware.org/gdb/onlinedocs/gdb/Extending-GDB.html)。最后向大家推荐一个github上的.gdbinit文件:https://github.com/gdbinit/Gdbinit/blob/master/gdbinit,把这个弄懂,相信gdb脚本文件就不在话下了。

参考文献:
(1)Extending GDB (https://sourceware.org/gdb/onlinedocs/gdb/Extending-GDB.html);
(2)捉虫日记(http://www.ituring.com.cn/book/909)。

 

 

UNIX/Linux C 程序员需要掌握的七种武器

我是一名普通的软件工程师,不是什么技术大牛。这篇文章所提到的“七种武器”只是我这些年工作经验的一点体会和感悟,如果有错误的地方,还请大家指正。

(一)C语言
作为一名C程序员,熟练掌握C语言是最基本的一项技能。关于如何学好C语言,以及C语言话题的讨论,网上有很多经典的文章,我就不一一列举了。在这里,我只想谈一点我个人的体会:刚毕业时,我来到一家比较大的软件公司工作,而公司的工作模式是每个人只负责一个小模块。这样工作两年后,我自认为我的C语言水平已经很高了。后来,我来到现在这家公司。由于工作需要,接触的东西也多了,开源项目也了解一些,才发现自己的C语言能力太一般了:原来宏可以这样写,指针可以这样用…等等。现在我在写代码时,常常会想:这个行为是C语言规范定义的吗?如果是,是C89还是C99?我现在用的编译器支持吗?如果不是C语言规范定义的,那么在程序运行的这个平台,行为是确定的吗?所以我建议大家平时可以多想想这些问题,查查资料,相信一定会对C语言有更深的理解。

(二)UNIX/Linux系统编程
在UNIX/Linux系统上开发程序,掌握系统编程API是必不可少的技能。而这方面的经典书籍都是一些大部头的英文著作,让人望而生畏。在这里,我谈一点自己的建议:我觉得可以先找一本口碑不错的中文书先看一下,了解一下都有哪些种类的API。这样以后用到时,再去精读经典英文著作里的相关章节,或是查man手册。此外,我觉得如果有时间,可以学习一下经典的开源项目,了解这些开源项目是如何使用这些API的。举个例子,Redis是很多人推荐的一个很不错的学习C语言的开源项目。我在阅读代码时,看到保存数据到文件时会用到fsync函数,我就会去深入地了解一下这个函数的作用。这样比单纯地去看那些著作效果要好。

(三)网络编程及相关知识
关于网络方面,我想从以下三点分享一下我的体会:
a)网络协议。在日常的工作中,大家接触和使用最多的无疑是TCP/IP协议族。此外,由于工作领域不同,也可能用到其它的协议。比方说,我主要做电信相关的程序开发,平时可能接触SCTP协议会更多一些。对于这些协议,我觉得掌握最基本的知识是必须的,其它的边边角角知识可以等到用时再查。举例来说,TCP协议的“三次握手”,“四次挥手”,“TIME-WAIT状态”这些基本的知识点要弄明白,其它的边角知识学完老不用忘得也快,还是用时google一下效率更高。
b)Socket编程。Socket编程的经典书籍一点不比讲系统编程的书薄,所以可以选一本相对薄点,口碑不错的精读一下,这样基本就掌握的百分之五、六十了。另外有时间还是看一下经典的开源代码。这里还拿Redis举例,Redis里关于处理网络连接和通信的代码量不大,而且基本涵盖了所有常见的UNIX平台,看完以后一定受益匪浅。
c)协议分析工具。TCPdump、snoop(Solaris平台工具)、wireshark等这些工具不仅能帮助我们抓取数据包,还能分析数据包,这对debug网络程序有非常大的帮助。所以,我们至少要掌握这些工具最常用的功能。此外,对于开放源代码的工具,我们更是可以从代码中学到很多知识。举例来说,我做短信相关的项目,就从wireshark的分析短信协议的代码里学到了很多东西,这使我对短信协议有了更清晰的理解。

(四)脚本编程能力
一提到脚本编程,大家首先想到的可能就是Bash shell脚本编程了。不错,在Unix/Linux上,Bash shell也许就是用的最广泛的脚本编程语言。我平时主要用Bash shell做两个方面的工作:a)用于编写监控服务脚本;b)写一些简单的单元测试脚本,比如循环发一些命令,等等。但是Bash shell的功能远远要比这些强大。我见过一些高手用Bash shell编程语言写出了很好玩的游戏,也有人做出了很cool的网络应用。所以建议大家有兴趣可以多了解一下Unix/Linux的这层“壳”。当然,你也可以选择学习Python、Perl、Ruby等。不过相比这些语言,Bash shell至少你不用自己去安装,而且它能做的事同样很强大。

(五)操作系统及CPU体系结构
也许有一天,你会碰到这样的情景:你的程序在Solaris上会发生core dump,在Linux上却运行的好好的。经过一番艰苦的debug,最后得到的原因是两种操作系统对线程的调度策略不一样,这会使一个对全局变量没有加锁就访问的bug在Linux上很难出现。所以你需要尽可能地去了解你使用的操作系统,这样无论对写程序还是debug都会有很大的帮助。比如,你需要了解进程的内存布局,这样你就知道栈和堆到底在内存的哪段空间,为什么内存写越界有时会core dump,有时没事。
除了操作系统,了解CPU的体系结构也是一门必修课。比方说,SPARC CPU要求字节对齐,而X86 CPU则没有这个要求。又比如SPARC CPU是大端模式,而X86 CPU是小端模式,这就要求你对像位域这样的定义要格外小心。你还要了解你使用的CPU的汇编语言,至少能大概看懂。因为有些时候,当你从C代码中找不出bug的原因时,就需要你“透过现象看本质”,从汇编代码层面看看到底发生了什么。

(六)编译器和调试器
“工欲善其事,必先利其器”。编译器负责把源代码生成可执行文件,而调试器则是在程序出现bug时,用来“降妖除魔”的不二神器。我会以我最熟悉的gcc和gdb为例子,来谈一下个人体会。
gcc有很多编译选项,除了要熟悉像-O,-g这些最基本的选项,我建议大家可以多了解一些其它不常见的选项。因为这些选项很可能帮助我们找到程序的一些bug。举个例子,在比较新的gcc版本里,增加了-fstack-protector这个选项,而它可以帮助我们检查到缓冲区溢出这种bug。此外,你还可能碰到这种情况,一个bug总是发生在程序优化后的版本,而不会出现在没经过优化的版本。所以,多了解你的编译器,你就可以更好地了解你的程序是如何生成的。
一个程序员不可能不碰到bug,而这个时候,调试器就是最好的工具。可以说,在遇到bug时调试技巧和手段是否丰富是衡量一个程序员的能力和水平的重要参考。除此以外,gdb另一个重要用途就是分析程序的core dump文件。我个人很喜欢看破案和推理的电视节目。我觉得程序的core dump文件可以比作“犯罪现场”,而gdb就是公安人员用来在现场提取线索的工具。对gdb越熟悉,就越能从core dump文件提取有价值的信息,也就越有助于我们定位到程序bug的“root cause”。

(七)DTrace/SystemTap
DTrace是由Sun的几位才华横溢的工程师开发的,最开始只支持Solaris操作系统,现在FreeBSD和Mac OS X也都支持了。Linux上类似的工具有SystemTap,也有人把DTrace移植到Linux上,不过效果似乎并不好。简单地说,DTrace可以几乎不会在对整个系统有任何性能影响下,让你了解你的程序所发生的一切。这对分析程序的热点(“Hot spot”),了解程序的执行流程,定位程序bug都有很大的帮助。有些时候,DTrace可能是你唯一的工具。举例来说,有个程序只发生在生产环境,而在实验室环境无法复现(当然,理论上任何bug都可以复现,只是我们没有找到复现条件。)。你不可能在你怀疑的代码打上断点,然后用gdb去调试。这时你只能借助于DTrace,通过它去了解程序到底是如何运行的,当时的变量值是什么。此外,DTrace还可以帮你了解操作系统的kernel,这会满足很多geek的好奇心。

以上就是我总结的UNIX/Linux C 程序员需要掌握的“七种武器”,同时也很感谢你能耐心地读完这篇文章。如果这篇小文没有让你觉得浪费时间,而是对你有哪怕一点小小的帮助,我的目的也就达到了。如果你有一些好的观点,也非常欢迎你能分享出来。谢谢!

分享几篇关于gcc和gdb的文章

海法Linux俱乐部是一群在以色列海法地区生活的Linux工程师定期组织的,分享在Linux下开发程序经验的聚会。网站是http://www.haifux.org/。其中有很多topic,涉及的范围很广,感兴趣的同学可以登陆这个网站看一下。我把其中和gcc和gdb相关的文章选出来,分享在下面:
gdb – customize it the way you want
Advanced GDB
Compiling Effectively for Cell with GCC
GCC Profile Guided Optimization

Source Insight的Rebuild project功能

当使用Source Insight时,有可能会出现有些定义的macro或function找不到(显示为黑),原因通常是source insight的数据库文件被破坏了,这时可以用“Rebuild Project”这个选项去重新生成数据库文件:选择“project->Rebuild Project->Re-Create the whole project from scratch”。详细介绍可参考:http://www.sourceinsight.com/docs35/ad920311.htm

Solaris搭建64位C语言开发环境

刚来公司时,公司的C程序还是32位的。后来我阅读了一些资料,觉得64位的程序才是真正的趋势,所以就开始尝试着开发64位的程序。这篇文章介绍如何在Solaris下搭建64位C语言开发环境,希望给需要的朋友一点帮助。

(1)gcc

Solaris的/usr/sfw/bin/gcc可以用来编译64位C程序,但是需要加-m64编译选项,否则默认编译出来的是32位程序。此外也可以从gcc的官网下载gcc源代码,自行编译安装,但是要注意编译出来的gcc需要是64位的。

(2)gdb

调试64位C程序需要64位的gdb,gdb的安装步骤如下(以7.6版本为例):

1)gunzip gdb-7.6.tar.gz
2)tar xvf gdb-7.6.tar
3)export CC=”/usr/sfw/bin/gcc -m64″
4)mkdir build_gdb
5)cd build_gdb
6)../gdb-7.6/configure –prefix=“/…/…(a folder path)”
7)make
8)make install

需要注意以下两点:

a)Solaris下的tar程序不支持”-z”选项,所以只能先调用gunzip,再调用tar,不能一步搞定:tar -xzf gdb-7.6.tar.gz。

b)目前gdb的最新版本是7.7,在Solaris下编译会有错误。解决办法也很简单,可以参考这篇文章

(3)参考资料

个人认为Oracle的这本《Solaris 64-bit Developer’s Guide》,是在Solaris下开发64位C程序最好的资料。每一位C语言开发者都应该看一下,相信都能受益匪浅。

libumem使用和源码分析系列文章(二)

在使用libumem时,大家可能都有一点好奇,为什么系统会使用libumem封装的malloc,free这些内存管理函数去替换libc中的内存管理函数?其实答案就在malloc.c中的如下代码:

#ifdef __GLIBC__
static void __attribute__((constructor)) umem_malloc_init_hook(void)
{
	if (__malloc_hook != umem_malloc_hook) {
		umem_startup(NULL, 0, 0, NULL, NULL);
		__malloc_hook = umem_malloc_hook;
		__free_hook = umem_free_hook;
		__realloc_hook = umem_realloc_hook;
		__memalign_hook = umem_memalign_hook;
	}
}

void (*__malloc_initialize_hook)(void) = umem_malloc_init_hook;

#else
void __attribute__((constructor))
__malloc_umem_init (void)
{
	umem_startup(NULL, 0, 0, NULL, NULL);
}
#endif

针对上述代码,我们只需关注#ifdef __GLIBC__……#else这个部分,因为替换libc中的内存管理函数的魔法就在这个部分:

(1)__attribute__((constructor))

__attribute__是gcc的关键字,它允许用户声明一些属性。而constructor属性的含义则是被修饰的函数要在main()函数执行前执行。相对应地,还有一个destructor属性,则是在main()函数结束时执行。这对这个属性的讨论,大家可以参考stackoverflow上的一篇帖子

(2)__malloc_hook,__free_hook,__realloc_hook,__memalign_hook

这些其实是GNU C库提供的一些变量,程序通过修改这些变量,可以达到使用自己定义的malloc,free这些函数的目的。详细的解释可以参考:http://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html

一个变量越界导致破坏堆栈的bug

前一段时间在商用系统上出现了core dump,原因是由于一个局部变量写越界导致堆栈被破坏了。在这里,我把这个bug分享一下,希望给需要的朋友一点帮助。简化的代码如下:

typedef struct
{
    ......
} A;

void func1(char *p)
{
    ......
}

void main(void)
{
    A a;
    A *p = &a;
    char b[10];

    ......
    /* 
     * 这个程序运行在SPARC处理器上, 而SPARC处理器的堆栈生长
     * 方向是从高地址向低地址生长的.
     * 这个函数里会把b这个数组写越界,导致p这个变量被破坏, 
     * 结果在下面访问p时,是一个无效的值,导致core dump.
     */
    func1(b);
    printf("%p\n", p);
    ......

    return;
}

通过分析这个bug,分享一下我的心得体会:
(1)SPARC处理器是当年SUN公司开发的一款RISC处理器,如果有朋友从事Solaris软件开发,应该还是会接触到的。我一直觉得作为底层软件工程师,还是应该对处理器结构(包括汇编语言)有尽可能多的了解,这样对我们分析和解决问题都会有很大帮助。比方说,在分析这个bug时,如果知道SPARC处理器的堆栈生长方向是从高地址向低地址生长的,就会让我们把注意力集中在变量定义在p后面的变量上,这样就有了分析问题的方向。在这里,我推荐一篇介绍SPARC处理器的文章(http://web.cecs.pdx.edu/~apt/cs577_2008/sparc_s.pdf),有兴趣的朋友可以看一下。
(2)当遇到堆栈上的变量被莫名其妙改变时,首先还是检查那些地址比被破坏变量地址低的变量。因为一般有可能引起内存越界的代码,像memcpy,都会破坏高地址的值。以这个bug为例。变量p在一开始就赋了值,以后对p都是读操作,没有写操作。现在p变成了一个无效的值,那么首先就应该怀疑访问定义在p后面的变量出了问题。