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)的改变,所以使用时一定要谨慎。

往GNU邮件组发邮件要用纯文本格式

昨天遇到gcc使用方面的一个问题,就往gcc-help(gcc-help@gcc.gnu.org)邮件组发了一封求助邮件。但是浏览归档(https://gcc.gnu.org/ml/gcc-help/)找不到我发的邮件,应该是没有发送成功。

今天在hellogcc的IRC里请教了一下,才知道应该用纯文本格式发送。我用的是QQ邮箱,没找到如何设置纯文本发送(后经好心网友发邮件提醒:在邮件编辑区之下的“其他选项”中勾选“纯文本”,就可以了)。于是就改用gmail的邮箱发送,果然一下就成功了。以后记住往GNU组织(包括gcc,gdb等所有的邮件组)发邮件一定要用纯文本格式。

P.S. 关于gmail如何使用纯文本发送邮件,可以参考这个链接:https://support.google.com/mail/answer/2645922?hl=zh-Hans

2014开源开发工具大会见闻录

上午因为家里有事,没能看到上午两个精彩话题,也辛苦了明杰和吴伟以及开源协会的各位同学。

中午到达,下午第一个演讲是ARM来的王哲宇介绍的pyOCD,中午发困导致精神松懈,所以体会不深,整体感觉是一个用PYTHON写的OCD软件。
osdt2014_1
附图一张,总让人觉得快要从讲台上掉下来的王同学。

然后就是一代宗师LUBA的话题,因为一些不可抗力,话题从Xposed for ART on Android换成了Beyond Compiler Engineering,一位同学竟然愤然提起了抗议:“原来不是ART话题吗?”。
我现在真想对这位同学说,ART什么都是浮云,没准哪天突然就跟Dalvik一样就随手被GOOGLE换掉了。但是LUBA话题谈的显然是能走的更远的。
话题前一段总结起来就是摩尔定律逐渐衰落后给编译器带来了巨大的机会,最后他还拿出了一张图,对今后40年内,哪一年哪项技术会推动前进,没错具体到年的,不知道这张图是怎么来的,不过台下所有弄编译器的同学心里都兴奋了,沸腾了。明杰同学之后就一直在问我:“编译器真的这么有前途?”
然后话锋一转,大师开心的说,可以开始讲技术了。之后对GCC系和LLVM系进行了比较和分析外加小八卦。
osdt2014_2
忙着创业每天只能睡3个小时,仍然准备了双份精彩话题(有一份Xposed不能讲)的LUBA大神。

接着是硬件人中的软件人软件人中的硬件人——翟哥的话题。我刚进场没看见他,后来才知道,他想演示的板子还没搞定,带着表坐在最后忙活着呢。
最后到时间时还是没搞定,在我的建议下改为现场举起板子让大家看一眼。
整个话题都围绕OPENOCD展开,不过我主要注意点集中到了这玩意脚本支持的部分用的TCL,于是作为一个饱受GDB testsuite中TCL虐待的GDB开发者,在话题讨论时对使用TCL脚本进行了丧心病狂的吐槽,还试图拉齐姚同学一起来吐槽。
齐姚同学不愧是和稀泥的高手,分析说是因为OPENOCD是德国人作的,德国人作事力图严谨,肯定会在通读过整个TCL手册才会开弄。
osdt2014_3
挥舞着板子,致力于向广大屌丝码农提供全栈开源的方式(软硬件都开源)搭建ARM处理器的JTAG调试环境的翟哥。

下午最后一个常规话题是专注于模拟等方向的SKYEYE开发者康老师,小小炫耀一下,我曾经也是SKYEYE开发者。
他的话题比较详细的介绍了符号执行以及其和其他程序分析技术的区别等等,之后又演示了他们的Android的全系统符号执行工具-Android_S2E。
osdt2014_4
话题时间对他来说永远不够,被我们安排到最后一个可以随便拖时间很开心的康老师。

之后两个是闪电演讲,先是一位阿里的同学讲了JAVASCRIPT的相关内容,我走神了没太听仔细,希望看视频能仔细看看。另外阿里工具组我们早有耳闻,希望明年能看见他们的常规话题。
osdt2014_5
我没记住名字的阿里同学。

之后是我的老友,和我一样也是曾经的模拟器开发者,现在专注Android安全的在读博士亚金。对他话题的提问都是问他用什么系统的手机,用什么Android ROM的。
osdt2014_6
专注吓唬Android用户,看我站在台下忍住没黑我小米的亚金。

之后是欢乐的抽奖活动,然后就是更欢乐的演讲者聚餐(最快乐的是吐槽了没到场的同学,老唐我们没说你啦,嘿嘿!),明年再见啦各位。

====================================================================

补几张照片,算是凑成整个大会的见闻录。(by xmj)

上午两场都非常精彩,嘉宾演讲得精彩,工作本身也做得非常出色。对做工具链的同学来说,是很好的学习参考。

qi yao
齐尧,给大家讲述了gdb性能方面的故事

kito
Kito,把经典的寄存器分配算法用实例给大家展现了一遍

中午,有一位报名自由演讲的朋友因为下午有事,改在了午餐后的休息时间,我不在现场,但看起来人气很旺。我这里只有一张,他自己用开源软硬件做的飞行器照片:

开源软硬件搭建的飞行器
开源软硬件做的飞行器

下午,最后是抽奖和合影:
抽奖
盒子,音箱和书,抽奖是个体力活

最后的合影:
合影
大家都笑了,是因为茶总喊了句玩笑口号:“打倒llvm”。

另外,luba给稍微有点绕口的大会名称起了一个好记的绰号,“开开工”。:-)

关于”error conflicting types for function”编译错误的分析

在使用gcc编译C程序时,有时会碰到“error: conflicting types for ‘function’”的编译错误。从字面意义上理解,是说函数的定义和声明不一致。在这篇文章里,我就对这个错误做个简单的分析(使用的gcc版本是4.9.0)。
(一)首先我们看一个函数的定义和声明不一致的例子:

#include <stdio.h>

int func(int a);

int func(void) {
    return 0;
}

int main(void) {

    func();

    return 0;
}

编译程序:

gcc -g -o a a.c
a.c:5:5: error: conflicting types for ‘func’
 int func(void) {
     ^
a.c:3:5: note: previous declaration of ‘func’ was here
 int func(int a);

可以看到由于“func”的声明和定义不一致(一个有参数,一个没有),所以编译时出现了这个错误。

(二)最近我在把一个老程序从Solaris移植到Linux,编译时也出现了这个错误。但是我发现函数在头文件里的声明和函数定义是完全一样的,这就令我很奇怪。查了将近一天时间,最后得到结论是函数参数类型在函数声明后定义了。简化的代码如下:

#include <stdio.h>

void func(struct A *A);

struct A {
        int a;
};

void func(struct A *A)
{
        printf("%d", A->a);
}

int main(void) {
        // your code goes here
        struct A a = {4};
        func(&a);
        return 0;
}

其中“structure A”的定义放在“func”函数声明之后了,而func函数的参数是“structure A*”类型。编译结果如下:

gcc -g -o a a.c
a.c:3:18: warning: ‘struct A’ declared inside parameter list
 void func(struct A *A);
                  ^
a.c:3:18: warning: its scope is only this definition or declaration, which is probably not what you want
a.c:9:6: error: conflicting types for ‘func’
 void func(struct A *A)
      ^
a.c:3:6: note: previous declaration of ‘func’ was here
 void func(struct A *A);
      ^

可以看到也输出了“error: conflicting types for ‘func’”的编译错误,也许编译警告可以给一点提示吧。

我检查了一下程序的Makefile,所有编译警告都关掉了,也许是编译警告太多了吧。

为什么gcc在64位Solaris上编译出来的程序默认是32位的?

最近发现一个问题,gcc在64位Solaris上编译出来的程序默认是32位的,而在64位Linux上编译出来的程序默认就是64位的,觉得有点奇怪,就在stackoverflow上问了一下(http://stackoverflow.com/questions/25560539/how-does-gcc-determine-if-to-generate-a-32-bit-or-64-bit-executable-file-by-defa)。其中一个回答给出了一个链接(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=58833),原来这是Solaris有意而为之。总结一下,有以下几点:

(1)64位的gcc或程序不一定比32位运行快;

(2)Studio程序默认是32位的,gcc最好和它行为保持一致;

(3)从用户体验出发,以前都是默认生成32位程序,现在一下变成64位,用户可能需要改很多配置;

(4)64位Solaris位的gcc可以既编译32位,又编译64位,看用户自己的选择了。