ARM平臺(海思)unwind棧回溯的實現

最初,第一次接觸到棧回溯是由於在追查不同的業務場景問題時,通常對方僅僅給你一個接口,而爲了弄清楚場景的調用方向,就需要問不同的人,嘗試不同的方法,自己想嘗試通過一種方法能夠加速對繁雜業務代碼的閱讀和理解。
而最近越來越覺得,在面對崩潰問題時,大家的無措,更是應該用signal捕捉結合棧回溯來完成的。

棧回溯的選擇

最初在做時,查了一些的方法,大致如下:(歡迎大家提意見補充)

  1. __builtin_return_address:在當前代碼中最常見的方式,獲取上層的調用者地址,但是缺點在許多ARM平臺上或者說看網上默認gcc都只能回溯一層,即只支持__builtin_return_address(0)
  2. -mapcs/-mapcs-frame:原理和之前學習ARM棧幀關係一樣,而該選項則是告知編譯器遵循APCS(ARM Procedure Call Standard)規範,APCS規範了arm寄存器的使用、函數調用過程出棧和入棧的約定,但是缺點是在複雜的代碼結構下,會造成編譯器內部錯誤而導致的編譯不過問題;
  3. -funwind-tables:也是最終採用的方式,也是抱着嘗試的心態,發現最終即使在複雜的代碼結構下,也能夠正常通過,其原理爲鏈接器實際保存了幀的解壓縮信息放置在專用鏈接器部分,而幀展開後的信息允許程序在任何點進行“窺視”上下文;

引用棧回溯的過程

  1. 初探:
    在決定並嘗試-funwind-tables可以編譯通過後,使用__Unwind_Backtrace__Unwind_GetIP完成了棧回溯,但是由於只能打印地址,每次需要使用nostrip的文件進行gdb查找函數名。
  2. 完善:
    在對這些不滿足的條件下,發現了dladdr函數的妙用,但是卻發現只有動態庫中的符號可以被準確的查找到,因此將可執行文件的部分相關庫改成了動態庫,這無非是最直接的方法了,而後接觸到的-rdynamic編譯選項再次讓我歎爲觀止,是爲了-funwind-tables而生的沒錯了。
  3. 補充嘗試:
    在順利將unwind應用到工程中的我又想將其應用於崩潰問題的查看,而真正讓我付諸實踐的是新同事的一句“怎麼我們現在還沒有一套完整查看崩潰問題的工具”,而這就是個契機。真正實現起來並不難,但是實際發現,崩潰時的調用棧只能打印signal捕捉函數本身,這纔是問題的關鍵。但是demo明明可用,一定是有哪個地方會影響到-funwind-tables,將工程的所有gcc編譯選項逐一比對發現在-O1/-O2/-O3/-Os的情況下會導致無法正常使用棧回溯(雖然後來發現即使加上-Os也能正常了,但是希望讀者可以在出現問題時往這方面去查)。最終將該工具部署後,signal捕捉常見的SIGSEGV/SIGABRT/SIGFPE後發現,還是存在部分崩潰無法棧回溯:
  • abort()
  • 0異常
  • 部分signal11的情況:空函數指針(unwind的固有缺陷)/mmap環形緩衝的memset空指針
    而如上問題,通過查資料和demo嘗試,pc下是可以正常回溯abort和除0的,因此一定還有哪裏不同於pc。

glibc交叉編譯

除了懷疑架構上的不同貌似已經沒有懷疑點了,但是之前的unwind原理上可以實現並且是架構無關的,應該還存在可以嘗試的點——glibc庫的重新編譯-funwind-tables,如下是參照網上的方法依然遇到的幾個坑:

  • 編譯方式1:
callon@callon-virtual-machine:~/Documents/glibc-2.16.0$ ./configure --prefix=/home/callon/Documents/out --host=arm-linux --enable-add-on=nptl CC=arm-hisiv400-linux-gcc CXX=arm-hisiv400-linux-g++

報錯1:

configure: error: you must configure in a separate build directory

解決1:

mkdir build/ out/;cd build/;../configure --prefix=/home/callon/Documents/glibc-2.16.0/out --host=arm-linux --enable-add-on=nptl CC=arm-hisiv400-linux-gcc CXX=arm-hisiv400-linux-g++
  • 報錯2:
checking sysdep dirs... configure: error: The arm is not supported.

解決2:網上的方法,下載glibc-ports-2.16.0,並解壓到glibc-2.16.0目錄下重命名爲ports目錄

  • 報錯3:
checking add-on ports for preconfigure fragments... alpha am33 arm Old ABI no longer supported

解決3:通過找no longer supported報錯的具體位置,找到是變量的值不對導致,

callon@callon-virtual-machine:~/Documents/glibc-2.16.0$ grep -nr "no longer supported" .
./ports/sysdeps/arm/preconfigure:45:		echo "Old ABI no longer supported" 2>&1
./README:25:Linux kernels is no longer supported, and we are not distributing it

最後修改--host爲海思編譯器原始的前綴後正常

callon@callon-virtual-machine:~/Documents/glibc-2.16.0/build$ ../configure --prefix=/home/callon/Documents/glibc-2.16.0/out --host=arm-hisiv400-linux-gnueabi --enable-add-on=nptl CC=arm-hisiv400-linux-gcc CXX=arm-hisiv400-linux-g++

而我們是爲了編譯出正常可回溯的glibc,因此,參考網上的方式,編譯選項整體爲

callon@callon-virtual-machine:~/Documents/glibc-2.16.0/build$ ../configure --prefix=/home/callon/Documents/glibc-2.16.0/out --host=arm-hisiv400-linux-gnueabi --enable-add-on=nptl CC=arm-hisiv400-linux-gcc CXX=arm-hisiv400-linux-g++ CFLAGS="-g -O2 -U_FORTIFY_SOURCE" libc_cv_forced_unwind=yes libc_cv_c_cleanup=yes

而最終在設備上:

mkdir /mnt/nfs;mount -t nfs -o nolock xxx.xx.xx.xx:/home/callon/nfs/test /mnt/nfs;cd /mnt/nfs
mkdir /libtmp;cp -d ./lib/* /libtmp
export LD_LIBRARY_PATH=/libtmp:$LD_LIBRARY_PATH
./test

發現abort依然無法回溯
在即將放棄時,使用

callon@callon-virtual-machine:~/Documents/glibc-2.16.0/build$ ../configure --prefix=/data1/huangkailun/work/glibc-2.16.0/out --host=arm-hisiv400-linux-gnueabi --enable-add-on=nptl CC=arm-hisiv400_v2-linux-gcc CXX=arm-hisiv400_v2-linux-g++ CFLAGS="-g -Os -funwind-tables"

最終成功回溯signal 6

signal 8多次嘗試都不行,再次反彙編深入追究其原因發現:

void test_func()
{
    841c:	e92d4800 	push	{fp, lr}
    8420:	e28db004 	add	fp, sp, #4
    8424:	e24dd008 	sub	sp, sp, #8
    int a = 0, b = 1;
    8428:	e3a03000 	mov	r3, #0
    842c:	e50b3008 	str	r3, [fp, #-8]
    8430:	e3a03001 	mov	r3, #1
    8434:	e50b300c 	str	r3, [fp, #-12]
    b /= a;
    8438:	e51b000c 	ldr	r0, [fp, #-12]
    843c:	e51b1008 	ldr	r1, [fp, #-8]
    8440:	eb000016 	bl	84a0 <__aeabi_idiv>
    8444:	e1a03000 	mov	r3, r0
    8448:	e50b300c 	str	r3, [fp, #-12]
    b += a;
    844c:	e51b200c 	ldr	r2, [fp, #-12]
    8450:	e51b3008 	ldr	r3, [fp, #-8]
    8454:	e0823003 	add	r3, r2, r3
    8458:	e50b300c 	str	r3, [fp, #-12]
}

結合網上查閱的資料發現實際上,對於滿足eabi(嵌入式arm應用程序二進制接口)的arm工具鏈,編譯時編譯器將編譯對象的’/'操作替換爲調用__aeabi_idiv函數,__aeabi_idiv是由libgcc.so或gcc.a庫提供的。
所以編譯glibc是不夠的,最好整個工具鏈的gcc庫都更新才行,而除0異常本身出現較少,因此不再深究。

但在多次的demo嘗試中,發現memcpy/memset/memmovesignal 11崩潰居然無法追溯,但是簡單的空指針賦值/strncpy等是正常的,而且更奇怪的是,默認的libc.so.6居然可以回溯memcpy,但是不能回溯memset,這樣就更加奇怪了,通過grep不斷的找memcpystrncpy這些函數到底有什麼不同時,發現glibc-ports中存在memset.S/memcpy.S/memmove.S,正好沒有strncpy的彙編實現函數,並且在string/memcpy.c中加上了printf的打印沒有打出來,而strncpy的是可以的,因此問題變成了memcpy如何使用.c的而不是.S的實現,中途嘗試過:

  1. 刪除.S的實現(編譯不過);
  2. Makefile中的相關編譯文件刪除(無影響);
  3. preconfigure文件中的-fno-unwind-tables選項刪除(…/configure運行不過),或者說執行後手動刪除所有-fno-unwind-tables的地方(無影響);
  4. 改變memcpy.SENTRY的名稱爲asm_memcpy(編譯錯誤很多)
    等等,以及網上查了好幾天方案也都沒有。

不過還好,經過不懈的懷疑到嘗試到反思,
最終的方案是,將string/中的memset.c/memcpy.c/memmove.c替換掉memset.S/memcpy.S/memmove.S
再進行編譯,此時編譯通過,嘗試原來的demo,果然都能正常回溯了!

最終集成工程時,可以

arm-hisiv400_v2-linux-strip *.so*

將庫進行strip減少內存使用,並在進程啓動時加上

LD_PRELOAD=/home/debug_lib/libc.so.6 ./my_program

保證對其他進程影響最小,且backtrace的封裝也使用了glibc的自帶源碼參考,自己做了一些修改,主要是backtrace_symbols的實現,因爲發現在堆越界時,再次調用malloc,此時出現malloc內部的assert,然後系統死鎖無法恢復,這個非常嚴重,所以寫了一種不再使用malloc的實現方式,通過局部變量的數組傳入,只要限制棧回溯的層數和合理限制result數組的大小,是不會有任何問題的:

/* Return backtrace of current program state.
   Copyright (C) 2008, 2009 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   Contributed by Kazu Hirata <[email protected]>, 2008.

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library.  If not, see
   <http://www.gnu.org/licenses/>.  */

#include "unwind_backtrace.h"

struct trace_arg
{
  void **array;
  int cnt, size;
};

#ifdef SHARED
static _Unwind_Reason_Code (*unwind_backtrace) (_Unwind_Trace_Fn, void *);
static _Unwind_VRS_Result (*unwind_vrs_get) (_Unwind_Context *,
					     _Unwind_VRS_RegClass,
					     _uw,
					     _Unwind_VRS_DataRepresentation,
					     void *);

static void *libgcc_handle;

static void
init (void)
{
  libgcc_handle = __libc_dlopen ("libgcc_s.so.1");

  if (libgcc_handle == NULL)
    return;

  unwind_backtrace = __libc_dlsym (libgcc_handle, "_Unwind_Backtrace");
  unwind_vrs_get = __libc_dlsym (libgcc_handle, "_Unwind_VRS_Get");
  if (unwind_vrs_get == NULL)
    unwind_backtrace = NULL;
}

/* This function is identical to "_Unwind_GetGR", except that it uses
   "unwind_vrs_get" instead of "_Unwind_VRS_Get".  */
static inline _Unwind_Word
unwind_getgr (_Unwind_Context *context, int regno)
{
  _uw val;
  unwind_vrs_get (context, _UVRSC_CORE, regno, _UVRSD_UINT32, &val);
  return val;
}

/* This macro is identical to the _Unwind_GetIP macro, except that it
   uses "unwind_getgr" instead of "_Unwind_GetGR".  */
# define unwind_getip(context) \
  (unwind_getgr (context, 15) & ~(_Unwind_Word)1)
#else
# define unwind_backtrace _Unwind_Backtrace
# define unwind_getip _Unwind_GetIP
#endif

static _Unwind_Reason_Code
backtrace_helper (struct _Unwind_Context *ctx, void *a)
{
  struct trace_arg *arg = a;

  /* We are first called with address in the __backtrace function.
     Skip it.  */
  if (arg->cnt != -1)
    arg->array[arg->cnt] = (void *) unwind_getip (ctx);
  if (++arg->cnt == arg->size)
    return _URC_END_OF_STACK;
  return _URC_NO_REASON;
}

int
backtrace (array, size)
     void **array;
     int size;
{
  struct trace_arg arg = { .array = array, .size = size, .cnt = -1 };
#ifdef SHARED
  __libc_once_define (static, once);

  __libc_once (once, init);
  if (unwind_backtrace == NULL)
    return 0;
#endif

  if (size >= 1)
    unwind_backtrace (backtrace_helper, &arg);

  if (arg.cnt > 1 && arg.array[arg.cnt - 1] == NULL)
    --arg.cnt;
  return arg.cnt != -1 ? arg.cnt : 0;
}

void
backtrace_symbols (array, size, result, max_len)
     void *const *array;
     int size;
     char **result;
     int max_len;
{
  Dl_info info[size];
  int status[size];
  int cnt;
  size_t total = 0;

  /* Fill in the information we can get from `dladdr'.  */
  for (cnt = 0; cnt < size; ++cnt)
    {
      struct link_map *map;
      status[cnt] = _dl_addr (array[cnt], &info[cnt], &map, NULL);
      if (status[cnt] && info[cnt].dli_fname && info[cnt].dli_fname[0] != '\0')
	{
	  /* We have some info, compute the length of the string which will be
	     "<file-name>(<sym-name>+offset) [address].  */
	  total += (strlen (info[cnt].dli_fname ?: "")
		    + strlen (info[cnt].dli_sname ?: "")
		    + 3 + WORD_WIDTH + 3 + WORD_WIDTH + 5);

	  /* The load bias is more useful to the user than the load
	     address.  The use of these addresses is to calculate an
	     address in the ELF file, so its prelinked bias is not
	     something we want to subtract out.  */
	  info[cnt].dli_fbase = (void *) map->l_addr;
	}
      else
	total += 5 + WORD_WIDTH;
    }

  if (result != NULL)
    {
      char *last = (char *) (result + size);

      for (cnt = 0; cnt < size; ++cnt)
	{
	  result[cnt] = last;

	  if (status[cnt]
	      && info[cnt].dli_fname != NULL && info[cnt].dli_fname[0] != '\0')
	    {
	      if (info[cnt].dli_sname == NULL)
		/* We found no symbol name to use, so describe it as
		   relative to the file.  */
		info[cnt].dli_saddr = info[cnt].dli_fbase;

	      if (info[cnt].dli_sname == NULL && info[cnt].dli_saddr == 0)
		last += 1 + sprintf (last, "%s(%s) [%p]",
				     info[cnt].dli_fname ?: "",
				     info[cnt].dli_sname ?: "",
				     array[cnt]);
	      else
		{
		  char sign;
		  long int offset;
		  if (array[cnt] >= (void *) info[cnt].dli_saddr)
		    {
		      sign = '+';
		      offset = array[cnt] - info[cnt].dli_saddr;
		    }
		  else
		    {
		      sign = '-';
		      offset = info[cnt].dli_saddr - array[cnt];
		    }

		  last += 1 + sprintf (last, "%s(%s%c%#tx) [%p]",
				       info[cnt].dli_fname ?: "",
				       info[cnt].dli_sname ?: "",
				       sign, offset, array[cnt]);
		}
	    }
	  else
	    last += 1 + sprintf (last, "[%p]", array[cnt]);
	}
      assert (last <= (char *) result + max_len);
    }

  return;
}

#ifdef SHARED
/* Free all resources if necessary.  */
libc_freeres_fn (free_mem)
{
  unwind_backtrace = NULL;
  if (libgcc_handle != NULL)
    {
      __libc_dlclose (libgcc_handle);
      libgcc_handle = NULL;
    }
}
#endif

發佈了20 篇原創文章 · 獲贊 65 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章