閱讀527 返回首頁    go 阿裏雲 go 技術社區[雲棲]


gcc的 “-fpack-struct” 編譯選項導致程序core dump的分析

最近team引入gcov來做代碼分析。編譯好的程序在Solaris上運行的好好的,結果在Linux上一運行就會產生core dump文件。這篇文章就介紹整個分析過程。

首先用gdb分析core文件,顯示是strlen調用出了問題:

(gdb) 
#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),使用這個編譯選項會導致ABI(Application Binary Interface)的改變,所以使用時一定要謹慎。

本文轉載自我在hellogcc上文章:https://www.hellogcc.org/?p=34087

最後更新:2017-05-23 15:36:57

  上一篇:go  如何編寫屬於自己的Java / Scala的調試器
  下一篇:go  ELRepo - Enterprise Linux ???RHEL???CentOS ??? SL??????????????????-??????-????????????-?????????