2024年6月22日 星期六

signal handle backtrace (1)

世間多不公, 以血引雷霆
上篇, glibc 的 backtrace 怎麼辦到在 signal handler 還可以正常抓出上一層的呼叫者, 先說答案, 我不知道, 看到這邊你可能會失望, 裡頭有 gcc 魔法, 我不是想往裡頭鑽, 所以這篇是介紹 glibc 的 backtrace 用什麼手法辦到這件事情。

__backtrace 有好幾個平台的實作, x86_64 應該是用 list 1 這個。

在 glibc 2.39 debug/backtrace.c __backtrace (void **array, int size) 可以看到類似的作法。

list 1. glibc-2.39/debug/backtrace.c
 1 /* Return backtrace of current program state.
 2    Copyright (C) 2003-2024 Free Software Foundation, Inc.
 3    This file is part of the GNU C Library.
 4
 5    The GNU C Library is free software; you can redistribute it and/or
 6    modify it under the terms of the GNU Lesser General Public
 7    License as published by the Free Software Foundation; either
 8    version 2.1 of the License, or (at your option) any later version.
 9
10    The GNU C Library is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    Lesser General Public License for more details.
14
15    You should have received a copy of the GNU Lesser General Public
16    License along with the GNU C Library; if not, see
17    <https://www.gnu.org/licenses/>.  */
18
19 #include <execinfo.h>
20 #include <stdlib.h>
21 #include <unwind.h>
22 #include <unwind-link.h>
23
24 struct trace_arg
25 {
26   void **array;
27   struct unwind_link *unwind_link;
28   _Unwind_Word cfa;
29   int cnt;
30   int size;
31 };
32
33 static _Unwind_Reason_Code
34 backtrace_helper (struct _Unwind_Context *ctx, void *a)
35 {
36   struct trace_arg *arg = a;
37
38   /* We are first called with address in the __backtrace function.
39      Skip it.  */
40   if (arg->cnt != -1)
41     {
42       arg->array[arg->cnt]
43 	= (void *) UNWIND_LINK_PTR (arg->unwind_link, _Unwind_GetIP) (ctx);
44       if (arg->cnt > 0)
45 	arg->array[arg->cnt]
46 	  = unwind_arch_adjustment (arg->array[arg->cnt - 1],
47 				    arg->array[arg->cnt]);
48
49       /* Check whether we make any progress.  */
50       _Unwind_Word cfa
51 	= UNWIND_LINK_PTR (arg->unwind_link, _Unwind_GetCFA) (ctx);
52
53       if (arg->cnt > 0 && arg->array[arg->cnt - 1] == arg->array[arg->cnt]
54 	 && cfa == arg->cfa)
55        return _URC_END_OF_STACK;
56       arg->cfa = cfa;
57     }
58   if (++arg->cnt == arg->size)
59     return _URC_END_OF_STACK;
60   return _URC_NO_REASON;
61 }
62
63 int
64 __backtrace (void **array, int size)
65 {
66   struct trace_arg arg =
67     {
68      .array = array,
69      .unwind_link = __libc_unwind_link_get (),
70      .cfa = 0,
71      .size = size,
72      .cnt = -1
73     };
74
75   if (size <= 0 || arg.unwind_link == NULL)
76     return 0;
77
78   UNWIND_LINK_PTR (arg.unwind_link, _Unwind_Backtrace)
79     (backtrace_helper, &arg);
80
81   /* _Unwind_Backtrace seems to put NULL address above
82      _start.  Fix it up here.  */
83   if (arg.cnt > 1 && arg.array[arg.cnt - 1] == NULL)
84     --arg.cnt;
85   return arg.cnt != -1 ? arg.cnt : 0;
86 }
87 weak_alias (__backtrace, backtrace)
88 libc_hidden_def (__backtrace)

看了一下 list 2, 是 t1 的反組譯, __backtrace 有看到 _Unwind_Backtrace, 所以應該是用 list 1 這段。

list 2. t1.dis
     1 
     2 t1:     file format elf64-x86-64
     3 
     4 
     5 Disassembly of section .init:


 82837 0000000000453080 <__backtrace>:
 82838   453080:	55                   	push   %rbp
 82839   453081:	48 89 e5             	mov    %rsp,%rbp
 82840   453084:	48 83 ec 20          	sub    $0x20,%rsp
 82841   453088:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
 82842   45308f:	00 00 
 82843   453091:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
 82844   453095:	31 c0                	xor    %eax,%eax
 82845   453097:	85 f6                	test   %esi,%esi
 82846   453099:	48 89 7d e0          	mov    %rdi,-0x20(%rbp)
 82847   45309d:	48 c7 45 e8 00 00 00 	movq   $0x0,-0x18(%rbp)
 82848   4530a4:	00 
 82849   4530a5:	c7 45 f0 ff ff ff ff 	movl   $0xffffffff,-0x10(%rbp)
 82850   4530ac:	89 75 f4             	mov    %esi,-0xc(%rbp)
 82851   4530af:	7e 34                	jle    4530e5 <__backtrace+0x65>
 82852   4530b1:	48 8d 75 e0          	lea    -0x20(%rbp),%rsi
 82853   4530b5:	48 8d 3d 44 ff ff ff 	lea    -0xbc(%rip),%rdi        # 453000 <backtrace_helper>
 82854   4530bc:	e8 2f 59 05 00       	callq  4a89f0 <_Unwind_Backtrace>
 82855   4530c1:	8b 45 f0             	mov    -0x10(%rbp),%eax
 82856   4530c4:	83 f8 01             	cmp    $0x1,%eax
 82857   4530c7:	7e 17                	jle    4530e0 <__backtrace+0x60>
 82858   4530c9:	48 8b 4d e0          	mov    -0x20(%rbp),%rcx
 82859   4530cd:	48 63 d0             	movslq %eax,%rdx
 82860   4530d0:	48 83 7c d1 f8 00    	cmpq   $0x0,-0x8(%rcx,%rdx,8)
 82861   4530d6:	75 0f                	jne    4530e7 <__backtrace+0x67>
 82862   4530d8:	83 e8 01             	sub    $0x1,%eax
 82863   4530db:	eb 0a                	jmp    4530e7 <__backtrace+0x67>
 82864   4530dd:	0f 1f 00             	nopl   (%rax)
 82865   4530e0:	83 f8 ff             	cmp    $0xffffffff,%eax
 82866   4530e3:	75 02                	jne    4530e7 <__backtrace+0x67>
 82867   4530e5:	31 c0                	xor    %eax,%eax
 82868   4530e7:	48 8b 4d f8          	mov    -0x8(%rbp),%rcx

list 3. t1.s
  1 	.file	"t1.c"
  2 	.text
  3 	.section	.rodata
  4 .LC0:
  5 	.string	"addr2line -f -e ./t1 %#lx\n"
  6 .LC1:
  7 	.string	"cmd: %s\n"
  8 	.text
  9 	.globl	addr2func
 10 	.type	addr2func, @function
 11 addr2func:
 12 .LFB5:
 13 	.cfi_startproc
 14 	pushq	%rbp
 15 	.cfi_def_cfa_offset 16
 16 	.cfi_offset 6, -16
 17 	movq	%rsp, %rbp
 18 	.cfi_def_cfa_register 6
 19 	subq	$160, %rsp
 20 	movq	%rdi, -152(%rbp)
 21 	movq	%fs:40, %rax
 22 	movq	%rax, -8(%rbp)
 23 	xorl	%eax, %eax
 24 	leaq	-144(%rbp), %rdx
 25 	movl	$0, %eax
 26 	movl	$16, %ecx
 27 	movq	%rdx, %rdi
 28 	rep stosq
 29 	movq	-152(%rbp), %rdx
 30 	leaq	-144(%rbp), %rax
 31 	leaq	.LC0(%rip), %rsi
 32 	movq	%rax, %rdi
 33 	movl	$0, %eax
 34 	call	sprintf@PLT
 35 	leaq	-144(%rbp), %rax
 36 	movq	%rax, %rsi
 37 	leaq	.LC1(%rip), %rdi
 38 	movl	$0, %eax
 39 	call	printf@PLT
 40 	leaq	-144(%rbp), %rax
 41 	movq	%rax, %rdi
 42 	call	system@PLT
 43 	movl	$0, %eax
 44 	movq	-8(%rbp), %rcx
 45 	xorq	%fs:40, %rcx
 46 	je	.L3
 47 	call	__stack_chk_fail@PLT
 48 .L3:
 49 	leave
 50 	.cfi_def_cfa 7, 8
 51 	ret
 52 	.cfi_endproc
 53 .LFE5:
 54 	.size	addr2func, .-addr2func
 55 	.globl	get_rip_value
 56 	.type	get_rip_value, @function
 57 get_rip_value:
 58 .LFB6:
 59 	.cfi_startproc
 60 	pushq	%rbp
 61 	.cfi_def_cfa_offset 16
 62 	.cfi_offset 6, -16
 63 	movq	%rsp, %rbp
 64 	.cfi_def_cfa_register 6
120 	.text
121 	.globl	print_backtrace
122 	.type	print_backtrace, @function
123 print_backtrace:
124 .LFB8:
125 	.cfi_startproc
126 	pushq	%rbp
127 	.cfi_def_cfa_offset 16
128 	.cfi_offset 6, -16
129 	movq	%rsp, %rbp
130 	.cfi_def_cfa_register 6
131 	subq	$960, %rsp
197 	ret
198 	.cfi_endproc
199 .LFE8:
200 	.size	print_backtrace, .-print_backtrace
228 	.text
229 	.globl	sig_handler
230 	.type	sig_handler, @function
231 sig_handler:
232 .LFB9:
233 	.cfi_startproc
234 	pushq	%rbp
235 	.cfi_def_cfa_offset 16
236 	.cfi_offset 6, -16
237 	movq	%rsp, %rbp
238 	.cfi_def_cfa_register 6
239 	subq	$80, %rsp
240 	movl	%edi, -68(%rbp)
241 	movl	$0, -52(%rbp)
242 	leaq	.LC6(%rip), %rdi
243 	call	puts@PLT
244 	movl	$0, %eax
413 	leave
414 	.cfi_def_cfa 7, 8
415 	ret
416 	.cfi_endproc
417 .LFE9:
418 	.size	sig_handler, .-sig_handler
419 	.section	.rodata
420 .LC18:
421 	.string	"sig thread pid: %d\n"
422 .LC19:
423 	.string	"sigwait"
424 	.align 8
425 .LC20:
426 	.string	"Signal handling thread got signal %d\n"
427 	.text
428 	.type	sig_thread, @function
429 sig_thread:
430 .LFB10:
431 	.cfi_startproc
432 	pushq	%rbp
433 	.cfi_def_cfa_offset 16
434 	.cfi_offset 6, -16
435 	movq	%rsp, %rbp
436 	.cfi_def_cfa_register 6
437 	subq	$48, %rsp
438 	movq	%rdi, -40(%rbp)
439 	movq	%fs:40, %rax
466 .L21:
467 	movl	-24(%rbp), %eax
468 	movl	%eax, %esi
469 	leaq	.LC20(%rip), %rdi
470 	movl	$0, %eax
471 	call	printf@PLT
472 	jmp	.L22
473 	.cfi_endproc
474 .LFE10:
475 	.size	sig_thread, .-sig_thread
476 	.section	.rodata
477 .LC21:
478 	.string	"123 thread pid: %d\n"
479 .LC22:
480 	.string	"123 thread tid: %d\n"
481 	.text
482 	.type	fun_123, @function
483 fun_123:
484 .LFB11:
485 	.cfi_startproc
486 	pushq	%rbp
487 	.cfi_def_cfa_offset 16
488 	.cfi_offset 6, -16
489 	movq	%rsp, %rbp
490 	.cfi_def_cfa_register 6
491 	subq	$32, %rsp
507 .L25:
508 	jmp	.L25
509 	.cfi_endproc
510 .LFE11:
511 	.size	fun_123, .-fun_123
512 	.type	fun_12, @function
513 fun_12:
514 .LFB12:
515 	.cfi_startproc
516 	pushq	%rbp
517 	.cfi_def_cfa_offset 16
518 	.cfi_offset 6, -16
519 	movq	%rsp, %rbp
520 	.cfi_def_cfa_register 6
521 	subq	$16, %rsp
522 	movq	%rdi, -8(%rbp)
523 	movq	-8(%rbp), %rax
524 	movq	%rax, %rdi
525 	call	fun_123
526 	nop
527 	leave
528 	.cfi_def_cfa 7, 8
529 	ret
530 	.cfi_endproc
531 .LFE12:
532 	.size	fun_12, .-fun_12
533 	.type	fun_1, @function
534 fun_1:
535 .LFB13:
536 	.cfi_startproc
537 	pushq	%rbp
538 	.cfi_def_cfa_offset 16
539 	.cfi_offset 6, -16
540 	movq	%rsp, %rbp
541 	.cfi_def_cfa_register 6
542 	subq	$16, %rsp
543 	movq	%rdi, -8(%rbp)
544 	movq	-8(%rbp), %rax
545 	movq	%rax, %rdi
546 	call	fun_12
547 	movl	$0, %eax
548 	leave
549 	.cfi_def_cfa 7, 8
550 	ret
551 	.cfi_endproc
552 .LFE13:
553 	.size	fun_1, .-fun_1
554 	.type	fun_223, @function
555 fun_223:
556 .LFB14:
557 	.cfi_startproc
558 	pushq	%rbp
559 	.cfi_def_cfa_offset 16
560 	.cfi_offset 6, -16
561 	movq	%rsp, %rbp
562 	.cfi_def_cfa_register 6
563 .L30:
564 	jmp	.L30
565 	.cfi_endproc
566 .LFE14:
567 	.size	fun_223, .-fun_223
568 	.type	fun_22, @function
569 fun_22:
570 .LFB15:
571 	.cfi_startproc
572 	pushq	%rbp
573 	.cfi_def_cfa_offset 16
574 	.cfi_offset 6, -16
575 	movq	%rsp, %rbp
576 	.cfi_def_cfa_register 6
577 	movl	$0, %eax
578 	call	fun_223
579 	nop
580 	popq	%rbp
581 	.cfi_def_cfa 7, 8
582 	ret
583 	.cfi_endproc
584 .LFE15:
585 	.size	fun_22, .-fun_22
586 	.type	fun_2, @function
587 fun_2:
588 .LFB16:
589 	.cfi_startproc
590 	pushq	%rbp
591 	.cfi_def_cfa_offset 16
592 	.cfi_offset 6, -16
593 	movq	%rsp, %rbp
594 	.cfi_def_cfa_register 6
595 	subq	$32, %rsp
596 	movq	%rdi, -24(%rbp)
605 	movl	$39, -12(%rbp)
606 	leaq	-12(%rbp), %rax
607 	movq	%rax, %rdx
608 	movl	$2, %esi
609 	movl	$0, %edi
610 	call	sched_setscheduler@PLT
611 .L33:
612 	movl	$0, %eax
613 	call	fun_22
614 	jmp	.L33
615 	.cfi_endproc
632 	.text
633 	.globl	main
634 	.type	main, @function
635 main:
636 .LFB17:
637 	.cfi_startproc
638 	pushq	%rbp
639 	.cfi_def_cfa_offset 16
640 	.cfi_offset 6, -16
641 	movq	%rsp, %rbp
642 	.cfi_def_cfa_register 6
643 	subq	$320, %rsp
644 	movl	%edi, -308(%rbp)
645 	movq	%rsi, -320(%rbp)
646 	movq	%fs:40, %rax
712 	jmp	.L37
713 	.cfi_endproc
714 .LFE17:
715 	.size	main, .-main
716 	.ident	"GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
717 	.section	.note.GNU-stack,"",@progbits

list 3 是 gcc 輸出的組合語言, 可以看到 .cfi 開頭的指令, 這並非是 x86 指令, 到底是什麼呢?

chatgpt 真的威, 連 .cfi_def_cfa 都可以解釋, 最重要的是用「中文」解釋。讓英文不好的我和英文好的開發者差距拉近了一點。以下是 gpt 的回答, 看完應該可以猜到 glibc 的 backtrace 是怎麼做的。

list 5. chatgpt 的回答
me: gcc 產生的 .s  .cfi_def_cfa 是什麼指令?

ChatGPT:
.s 文件是经过编译器(如 GCC)处理后生成的汇编代码文件,其中包含了对源代码进行编译后的汇编语言代码。.cfi_def_cfa 是 GCC 生成的用于描述函数调用框架信息的指令,用于指定 CFA(Canonical Frame Address)的定义方式。

具体来说,.cfi_def_cfa 指令用于指定当前函数中的 CFA(规范化帧地址)的定义方式。CFA 是指在函数调用过程中用来定位当前栈帧的地址。.cfi_def_cfa 指令有以下格式:

.cfi_def_cfa register, offset
register:指定一个寄存器,用来存储当前栈帧的基址指针(Base Pointer,BP)。
offset:指定相对于 register 的偏移量,表示 CFA 在当前栈帧中的位置。
例如,下面是一个示例 .s 文件中的 .cfi_def_cfa 指令的用法:

assembly
.text
.globl main

main:
    .cfi_startproc            ; 开始定义过程
    pushq %rbp                ; 保存旧的基址指针
    .cfi_def_cfa %rsp, 16     ; 定义 CFA 为栈顶寄存器,并在栈上向下偏移16字节
    pushq %rbx                ; 保存其他寄存器
    pushq %r12
    pushq %r13
    pushq %r14
    pushq %r15
    .cfi_endproc              ; 结束定义过程

    ret                        ; 返回
在这个示例中,.cfi_def_cfa %rsp, 16 指令指定了 CFA 的定义方式为栈顶寄存器 %rsp,并且在栈上向下偏移了16字节。这样可以帮助调试器在调试过程中准确地定位当前栈帧的位置,从而更方便地进行调试和分析。

me: 
        .cfi_def_cfa 7, 8
這是什麼意思?

ChatGPT:
.cfi_def_cfa 指令用于指定当前函数中的 CFA(Canonical Frame Address,规范化帧地址)的定义方式。CFA 是在函数调用过程中用来定位当前栈帧的地址。

.cfi_def_cfa register, offset
在你提供的 .cfi_def_cfa 7, 8 中,意思是指定了当前栈帧的 CFA 的定义方式。具体来说:

register:7 表示的是 RSP 寄存器,即栈顶指针。
offset:8 表示的是相对于 RSP 寄存器的偏移量,即栈帧在当前栈中的位置。
因此,.cfi_def_cfa 7, 8 指令表示当前栈帧的 CFA 是 RSP 寄存器,并且在当前栈中的位置偏移为 8 字节。这个信息通常用于调试目的,帮助调试器在调试过程中准确定位当前栈帧的位置。

me:
如何移除 .cfi_def_cfa 指令

ChatGPT:
在汇编代码中移除 .cfi_def_cfa 指令可能会导致一些调试信息丢失,因为这些指令是用于描述调用框架信息的。但是,如果你确实需要移除这些指令,可以通过以下方法之一实现:

手动删除:在汇编代码文件中手动删除所有 .cfi_def_cfa 指令。可以通过文本编辑器查找并删除这些指令。

编译器选项:在编译时使用编译器选项来禁用生成 .cfi_def_cfa 指令。例如,在使用 GCC 编译器时,可以使用 -fno-asynchronous-unwind-tables 选项来禁用生成调用框架信息。示例命令如下:

gcc -fno-asynchronous-unwind-tables your_program.c -o your_program
请注意,禁用调用框架信息可能会影响调试能力和代码执行的稳定性,因此谨慎使用。

优化选项:在编译时使用优化选项可能会移除部分调试信息,包括 .cfi_def_cfa 指令。可以使用 -O 选项来启用优化。示例命令如下:

gcc -O2 your_program.c -o your_program
这些方法中,手动删除指令可能是最直接的方法,但请谨慎操作,确保了解这些指令的作用和影响。在生产环境中,一般不建议删除这些调试信息,因为它们对调试和错误定位非常有用。

所以程式用了 -fno-asynchronous-unwind-tables, 就不會有 .cfi 這些指令, 然後你就會發現 backtrace 再也無法正確印出整個呼叫順序了。如 list 6, 只能抓到 print_backtrace, 我自己尋訪 stack frame 的作法依然可以正常。gcc 勝之不武, 偷偷用了自己的加料手法完成這件事情, 這樣也許也可以不因為 cpu 平台而有一個統一的作法吧。

list 6. 加入 -fno-asynchronous-unwind-tables
backtrace() returned 1 addresses
nn [0x400cfc]
cmd: addr2line -f -e ./t1 0x400cfc

print_backtrace
??:?

ref:
What do the CFI directives mean? (and some more questions)

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。