C语言的副作用

测试例子1:

#include 

int main(void) {
  int a = 5;
  printf("%d, %d, %d\n", (a++) , (a++) , (a++));
  printf("a = %d\n", a);
  return 0;
}

编译执行:

$ gcc foo.c
$ ./a.out 
7, 6, 5
a = 8

测试例子2:

#include 

int main(void) {
  int a = 5;
  printf("s = %d\n", (a++) + (a++) + (a++));
  printf("a = %d\n", a);
  return 0;
}

编译执行:

$ gcc-4.7 foo.c
$ ./a.out 
s = 15
a = 8
$ gcc-4.8 foo.c
$ ./a.out 
s = 18
a = 8

原因在于C语言标准中关于side effect和sequence point的描述和规范。这里有篇博文,介绍的非常到位(多谢kito-cheng提供)http://blog.tinlans.org/2010/08/06/sequence-point/

C语言标准中的原文如下:

The result of the postfix ++ operator is the value of the operand. After the result is
obtained, the value of the operand is incremented. (That is, the value 1 of the appropriate
type is added to it.) See the discussions of additive operators and compound assignment
for information on constraints, types, and conversions and the effects of operations on
pointers. The side effect of updating the stored value of the operand shall occur between
the previous and the next sequence point.
Evaluation of an expression may produce side effects. At
certain specified points in the execution sequence called sequence points, all side effects
of previous evaluations shall be complete and no side effects of subsequent evaluations
shall have taken place.

gdb无法调试最新gcc编译的程序

Reading symbols from /home/foo/helloword...done.
(gdb) start
Temporary breakpoint 1 at 0x4004dc
Starting program: /home/foo/helloword 

Temporary breakpoint 1, 0x00000000004004dc in main ()
(gdb) n
Single stepping until exit from function main,
which has no line number information.
Hello World
__libc_start_main (main=0x4004d8 
, argc=1, ubp_av=0x7fffffffc2b8, init=, fini=, rtld_fini=, stack_end=0x7fffffffc2a8) at libc-start.c:258 258 exit (result); (gdb)

这里,gcc是较新的版本,gdb版本较旧。原因是因为,gcc从4.8开始缺省使用了-gdwarf-4选项,较旧的gdb无法识别dwarf4版本的调试信息(多谢jasonwucj的指点)。参见

https://gcc.gnu.org/gcc-4.8/changes.html

当无法更新gdb的时候,则可以在用gcc编译程序时,使用选项-gdwarf-3来指定生成dwarf3版本的调试信息,这样就可以了。

小技巧:追溯历史开发信息

在gcc中,如果想查看以往的代码变动信息,可以查看相应源码目录下的ChangeLog文件。ChangeLog有很好的格式要求和内容规范,并且也要经过review。这虽然对于Patch提交者来来说显得有些繁琐,但当你需要查看历史开发信息的时候,就充分感受到了它的重要性了。

在LLVM中,没有ChangeLog文件,LLVM的开发者认为查看svn log就足够了。但是,日志信息的格式和内容比较随意,而且,一般还需要手动导出到文件中进行查看。使用svn命令时,建议使用svn log -v命令,这个-v选项,可以打印出相关的文件变动信息,可以帮助你更好的追溯代码变动情况。

一键编译安装gcc

今天看到一篇文章,作者通过一个shell脚本和一个Makefile,可以自动下载需要的所有安装包并且编译gcc

$ # Download the scripts using wget.
$ mkdir /opt/gcc-4.9.2
$ cd /opt/gcc-4.9.2
$ wget http://projects.joelinoff.com/gcc-4.9.2/bld.sh
$ wget http://projects.joelinoff.com/gcc-4.9.2/Makefile
$ chmod a+x bld.sh
$ make
[output snipped]
$ # The compiler is installed in /opt/gcc-4.9.2/rtf/bin

我试了一下,果然很方便。只要有一台可以联网的机器就可以了。感兴趣的朋友可以试一试。

P.S.:
(1)编译libiconv时可能会有“'gets' undeclared here“错误,请参考这篇文章
(2)如果机器是64位的,但是缺少32位的库文件。这样在编译gcc时会出现错误:“configure: error: I suspect your system does not have 32-bit developement libraries (libc and headers). If you have them, rerun configure with –enable-multilib. If you do not have them, and want to build a 64-bit-only compiler, rerun configure with –disable-multilib.”。提示需要配置“--disable-multilib”。

gcc的 “-fpack-struct” 编译选项导致程序core dump的分析

最近team引入gcov来做代码分析。编译好的程序在Solaris上运行的好好的,结果在Linux上一运行就会产生core dump文件。这篇文章就介绍整个分析过程。

首先用gdb分析core文件,显示是strlen调用出了问题:

(gdb) bt
#0  0x00000034e433386f in __strlen_sse42 () from /lib64/libc.so.6
#1  0x000000000053c57a in __gcov_init ()
#2  0x000000000053c4b9 in _GLOBAL__I_65535_0_g_cmd_param () at source/rerun/aicent_ara_rerun.c:963
#3  0x000000000053dc26 in __do_global_ctors_aux ()
#4  0x0000000000403743 in _init ()
#5  0x00007fff6d6b3ce8 in ?? ()
#6  0x000000000053db55 in __libc_csu_init ()
#7  0x00000034e421ecb0 in __libc_start_main () from /lib64/libc.so.6
#8  0x0000000000404449 in _start ()

由于我们使用的gcc是用安装包形式安装的,没有源码。所以就从github上找了相应版本的gcc源代码,希望能有所帮助。以下是__gcov_init函数的代码(https://github.com/gcc-mirror/gcc/blob/gcc-4_4_7-release/gcc/libgcov.c):

void
__gcov_init (struct gcov_info *info)
{
  if (!info->version)
    return;
  if (gcov_version (info, info->version, 0))
    {
      const char *ptr = info->filename;
      gcov_unsigned_t crc32 = gcov_crc32;
      size_t filename_length =  strlen(info->filename);

      /* Refresh the longest file name information */
      if (filename_length > gcov_max_filename)
        gcov_max_filename = filename_length;

      do
    {
      unsigned ix;
      gcov_unsigned_t value = *ptr << 24;

      for (ix = 8; ix--; value <<= 1)
        {
          gcov_unsigned_t feedback;

          feedback = (value ^ crc32) & 0x80000000 ? 0x04c11db7 : 0;
          crc32 <<= 1;
          crc32 ^= feedback;
        }
    }
      while (*ptr++);

      gcov_crc32 = crc32;

      if (!gcov_list)
    atexit (gcov_exit);

      info->next = gcov_list;
      gcov_list = info;
    }
  info->version = 0;
}

结合源代码和core文件可以看出,应该是“size_t filename_length = strlen(info->filename);”这一行出了问题。再结合汇编程序:

(gdb) disassemble __strlen_sse42
Dump of assembler code for function __strlen_sse42:
   0x00000034e4333860 <+0>:     pxor   %xmm1,%xmm1
   0x00000034e4333864 <+4>:     mov    %edi,%ecx
   0x00000034e4333866 <+6>:     mov    %rdi,%r8
   0x00000034e4333869 <+9>:     and    $0xfffffffffffffff0,%rdi
   0x00000034e433386d <+13>:    xor    %edi,%ecx
=> 0x00000034e433386f <+15>:    pcmpeqb (%rdi),%xmm1

是访问rdi寄存器出了问题,而rdi保存的应该是strlen的参数,也就是“info->filename”。试着访问一下rdi寄存器保存的地址:

(gdb) i registers rdi
rdi            0x57c4ac00000000 24704565987246080
(gdb) x/16xb 0x57c4ac00000000
0x57c4ac00000000:       Cannot access memory at address 0x57c4ac00000000

可以看到rdi寄存器保存的地址的确是个无效的地址。

接下来,就要分析一下为什么传入__gcov_initinfo结构体的filename是一个无效指针。首先看一下gcov_info结构体的定义(https://github.com/gcc-mirror/gcc/blob/gcc-4_4_7-release/gcc/gcov-io.h):

/* Information about a single object file.  */
struct gcov_info
{
  gcov_unsigned_t version;  /* expected version number */
  struct gcov_info *next;   /* link to next, used by libgcov */

  gcov_unsigned_t stamp;    /* uniquifying time stamp */
  const char *filename;     /* output file name */

  unsigned n_functions;     /* number of functions */
  const struct gcov_fn_info *functions; /* table of functions */

  unsigned ctr_mask;        /* mask of counters instrumented.  */
  struct gcov_ctr_info counts[0]; /* count data. The number of bits
                     set in the ctr_mask field
                     determines how big this array
                     is.  */
};

查看调用__gcov_init_GLOBAL__I_65535_0_g_cmd_param函数的汇编代码:

(gdb) disassemble _GLOBAL__I_65535_0_g_cmd_param
Dump of assembler code for function _GLOBAL__I_65535_0_g_cmd_param:
   0x000000000053c4ab <+0>:     push   %rbp
   0x000000000053c4ac <+1>:     mov    %rsp,%rbp
   0x000000000053c4af <+4>:     mov    $0x78d4a0,%edi
   0x000000000053c4b4 <+9>:     callq  0x53c4c0 <__gcov_init>
   0x000000000053c4b9 <+14>:    leaveq
   0x000000000053c4ba <+15>:    retq
End of assembler dump.

可以看到传入__gcov_init的参数为0x78d4a0,也就是指向gcov_info结构体的地址,查看这个地址的内容:

(gdb) x/64xb 0x78d4a0
0x78d4a0:       0x52    0x34    0x30    0x34    0x00    0x00    0x00    0x00
0x78d4a8:       0x00    0x00    0x00    0x00    0x82    0xf0    0xc7    0xa5
0x78d4b0:       0x60    0xc4    0x57    0x00    0x00    0x00    0x00    0x00
0x78d4b8:       0x0b    0x00    0x00    0x00    0xac    0xc4    0x57    0x00
0x78d4c0:       0x00    0x00    0x00    0x00    0x01    0x00    0x00    0x00
0x78d4c8:       0x39    0x01    0x00    0x00    0xc0    0xa4    0x47    0x03
0x78d4d0:       0x00    0x00    0x00    0x00    0xe0    0xd8    0x53    0x00
0x78d4d8:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

可以看到对应filename成员的值应该为0x57c4ac0000000b,的确是个无效地址。问题分析到这里,没了思路。后来,在gccbugzilla里找到这个问题:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=43341,才搞清楚是“-fpack-struct=4”这个编译选项导致的。

我们使用的是64位Linux,默认编译生成的可执行文件是64位的。所以gcov_info的默认内存布局应该是(gcov_unsigned_t类型占4个字节,指针类型占8个字节):

Offset 4 bytes 4 bytes
0 version 填充成员
8 next next
16 stamp 填充成员
24 filename filename

当使用“-fpack-struct=4”这个编译选项后,gcov_info的内存布局变为:

Offset 4 bytes 4 bytes
0 version next
8 next stamp
16 filename filename

经过推算,filename成员的值应该为0x57c460,验证一下:

(gdb) p (char*)0x57c460
$1 = 0x57c460 "/home/.../.....gcda"

打印出的是正确的值。在Solaris上没问题的原因是因为64位Solaris默认编译出来的程序是32位的。

看了一下gcc网站对-fpack-struct的介绍(https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#index-fpack-struct-2675),使用这个编译选项会导致ABI(Application Binary Interface)的改变,所以使用时一定要谨慎。