ARM中DWARF信息中的本地变量位置

14

我有一个C程序,保存在delay.c文件中:

void delay(int num)
{
   volatile int i;
   for(i=0; i<num; i++);
}

然后我在 ARM 模拟器(更具体地说是 armel)上使用 gcc 4.6.3 编译该程序,并使用以下命令 gcc -g -O1 -o delay.o delay.c。在 delay.o 中的汇编代码如下:

00000000 <delay>:
   0:   e24dd008    sub sp, sp, #8
   4:   e3a03000    mov r3, #0
   8:   e58d3004    str r3, [sp, #4]
   c:   e59d3004    ldr r3, [sp, #4]
  10:   e1500003    cmp r0, r3
  14:   da000005    ble 30 <delay+0x30>
  18:   e59d3004    ldr r3, [sp, #4]
  1c:   e2833001    add r3, r3, #1
  20:   e58d3004    str r3, [sp, #4]
  24:   e59d3004    ldr r3, [sp, #4]
  28:   e1530000    cmp r3, r0
  2c:   bafffff9    blt 18 <delay+0x18>
  30:   e28dd008    add sp, sp, #8
  34:   e12fff1e    bx  lr

我想通过调试信息来确定变量 i 在函数 delay 的堆栈上的位置。以下是在 .debug_info 部分中关于 delayi 的信息:

<1><25>: Abbrev Number: 2 (DW_TAG_subprogram)
   <26>   DW_AT_external    : 1
   <27>   DW_AT_name        : (indirect string, offset: 0x19): delay
   <2b>   DW_AT_decl_file   : 1
   <2c>   DW_AT_decl_line   : 1
   <2d>   DW_AT_prototyped  : 1
   <2e>   DW_AT_low_pc      : 0x0
   <32>   DW_AT_high_pc     : 0x38
   <36>   DW_AT_frame_base  : 0x0      (location list)
   <3a>   DW_AT_sibling     : <0x59>
...
<2><4b>: Abbrev Number: 4 (DW_TAG_variable)
   <4c>   DW_AT_name        : i
   <4e>   DW_AT_decl_file   : 1
   <4f>   DW_AT_decl_line   : 3
   <50>   DW_AT_type        : <0x60>
   <54>   DW_AT_location    : 0x20     (location list)

这表明 i 的位置在位置列表中。所以我输出了位置列表:

Offset   Begin    End      Expression
00000000 00000000 00000004 (DW_OP_breg13 (r13): 0)
00000000 00000004 00000038 (DW_OP_breg13 (r13): 8)
00000000 <End of list>
00000020 0000000c 00000020 (DW_OP_fbreg: -12)
00000020 00000024 00000028 (DW_OP_reg3 (r3))
00000020 00000028 00000038 (DW_OP_fbreg: -12)
00000020 <End of list>

从地址4到38,delay的帧基应为r13 + 8。所以从地址c到20和从地址28到38,i的位置是r13 + 8 - 12 = r13 - 4

然而,从汇编代码中可以得知,并没有r13 - 4这个位置,而i的位置显然是在r13 + 4

我是否忽略了某些计算步骤?有人能解释一下调试信息和汇编代码中i位置的差异吗?

先谢谢!

2个回答

5

TL;DR 该问题的分析是正确的,不一致是gcc组件中的一个错误(GNU Arm嵌入式工具链是记录错误的明显地方)。

目前情况下,这个答案 是不正确的,因为它错误地将位置表达式评估时堆栈指针的值与函数进入时较早的堆栈指针值混淆了。

就DWARF而言,i的位置随着程序计数器而变化。例如,考虑文本地址delay+0x18。此时,i的位置由DW_OP_fbreg(-12)给出,即相对于帧基址向下12字节。帧基址由父级DW_TAG_subprogram的DW_AT_frame_base属性给出,而在这种情况下,它也取决于程序计数器:对于delay+0x18,其表达式为DW_OP_breg13(8),即r13+8。重要的是,此计算使用当前r13的值,即当程序计数器等于delay+0x18时r13的值。
因此,DWARF断言,在delay+0x18处,i位于r13 + 8 - 12,即现有堆栈底部的下方4个字节。检查汇编代码显示,在delay+018处,应该在堆栈底部上方4个字节找到i。因此,DWARF存在错误,生成它的任何内容都有缺陷。
可以使用简单的包装器来证明这个漏洞,使用gdb测试用例提供了问题。
$ cat delay.c
void delay(int num)
{
   volatile int i;
   for(i=0; i<num; i++);
}
$ gcc-4.6 -g -O1 -c delay.c
$ cat main.c
void delay(int);

int main(int argc, char **argv) {
    delay(3);
}
$ gcc-4.6 -o test main.c delay.o
$ gdb ./test
.
.
.
(gdb) 

delay+0x18 处设置断点并运行到第二次出现(我们期望 i 为1):

(gdb) break *delay+0x18
Breakpoint 1 at 0x103cc: file delay.c, line 4.
(gdb) run
Starting program: /home/pi/test 

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) cont
Continuing.

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb)

我们从反汇编中得知,i在栈指针上方四个字节处。确实如此,它就在那里。
(gdb) print *((int *)($r13 + 4))
$1 = 1
(gdb)

然而,虚假的DWARF意味着gdb查找错误的位置:

(gdb) print i
$2 = 0
(gdb)

正如上面所解释的那样,DWARF不正确地给出了i的位置,即在堆栈指针下方四个字节处。 那里有一个零,因此报告了i的值:
(gdb)  print *((int *)($r13 - 4))
$3 = 0
(gdb)

这不是巧合。当要求gdb打印i时,位于栈指针下方的虚假位置中写入的一个幻数会重新出现。
(gdb) set *((int *)($r13 - 4)) = 42
(gdb) print i
$6 = 42
(gdb)

因此,在delay+0x18处,DWARF错误地将i的位置编码为r13 - 4,尽管其真实位置是r13 + 4
可以通过手动编辑编译单元并将DW_OP_fbreg(-12)(字节0x91 0x74)替换为DW_OP_fbreg(-4)(字节0x91 0x7c)来进一步操作。这会得到:
$ readelf --debug-dump=loc delay.modified.o 
Contents of the .debug_loc section:

Offset   Begin            End              Expression
00000000 00000000 00000004 (DW_OP_breg13 (r13): 0)
0000000c 00000004 00000038 (DW_OP_breg13 (r13): 8)
00000018 <End of list>
00000020 0000000c 00000020 (DW_OP_fbreg: -4)
0000002c 00000024 00000028 (DW_OP_reg3 (r3))
00000037 00000028 00000038 (DW_OP_fbreg: -4)
00000043 <End of list>

$

换句话说,DWARF已经被修正,以便在例如delay+0x18处,i的位置被给定为frame base - 4 = r13 + 8 - 4 = r13 + 4,与汇编匹配。使用修正后的DWARF重复gdb实验,每次循环都显示了预期值的i
$ gcc-4.6 -o test.modified main.c delay.modified.o
$ gdb ./test.modified 
.
.
.
(gdb) break *delay+0x18
Breakpoint 1 at 0x103cc: file delay.c, line 4.
(gdb) run
Starting program: /home/pi/test.modified 

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) print i
$1 = 0
(gdb) cont
Continuing.

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) print i
$2 = 1
(gdb) cont
Continuing.

Breakpoint 1, 0x000103cc in delay (num=3) at delay.c:4
4      for(i=0; i<num; i++);
(gdb) print i
$3 = 2
(gdb) cont
Continuing.
[Inferior 1 (process 30954) exited with code 03]
(gdb) 

2

我不同意楼主的汇编分析:

00000000 <delay>:                         ; so far, let's suppose sp = sp(0) 
0:   e24dd008    sub sp, sp, #8           ; sp = sp(0) - 8
4:   e3a03000    mov r3, #0               ; r3 = 0 
8:   e58d3004    str r3, [sp, #4]         ; store the value of r3 in (sp + 4)
c:   e59d3004    ldr r3, [sp, #4]         ; load (sp + 4) in r3
10:   e1500003    cmp r0, r3              ; compare r3 and r0  
14:   da000005    ble 30 <delay+0x30>     ; go to end of loop
18:   e59d3004    ldr r3, [sp, #4]        ; i is in r3, and it is being loaded from
                                          ; (sp + 4), that is, 
                                          ; sp(i) = sp(0) - 8 + 4 = sp(0) - 4
1c:   e2833001    add r3, r3, #1          ; r3 = r3 + 1, that is, increment i
20:   e58d3004    str r3, [sp, #4]        ; store i (which is in r3) in (sp + 4),
                                          ; being again sp(i) = sp(0) - 8 + 4 = \
                                          ; sp(0) - 4
24:   e59d3004    ldr r3, [sp, #4]        ; load sp + 4 in r3
28:   e1530000    cmp r3, r0              ; compare r3 and r0
2c:   bafffff9    blt 18 <delay+0x18>     ; go to init of loop
30:   e28dd008    add sp, sp, #8          ; sp = sp + 8
34:   e12fff1e    bx  lr                  ; 

所以,i位于sp(0)-4,这与DWAFR分析相匹配(它表示i位于0+8-12)。
编辑以添加有关我的DWARF分析的信息:
根据此行:00000020 0000000c 00000020 (DW_OP_fbreg: -12),其中DW_OP_fbreg为:
引用:

DW_OP_fbreg操作提供了从当前函数的DW_AT_frame_base属性中指定的地址的带符号LEB128偏移量。 (通常是“堆栈指针”寄存器加上或减去一些偏移量。在更复杂的系统上,它可能是一个位置列表,根据PC的变化调整偏移量,而PC的变化会影响堆栈指针。)

因此,地址为frame_base + offset,其中:
  • frame_base:是堆栈指针+/-某些偏移量,并且根据前一行(00000000 00000004 00000038 (DW_OP_breg13(r13):8)),从00000004到00000038,其偏移量为+8(r13为SP)
  • offset:显然是-12
鉴于此,DWARF表明它指向sp(0)+8-12=sp(0)-4

根据OP的分析,DWARF表示isp(0) - 12,但汇编显示它在sp(0) - 4 - Robert Harris
根据我的理解,OP的分析表明,根据汇编语言,i位于SP中,并带有+4的偏移量([...显然在位置'r13 + 4'...]),但是Dwarf报告说i是SP的偏移量为-4([... 'i'的位置为'r13 + 8-12 = r13-4' ...])。在我看来,矮人的分析是有道理的(0 + 8-12,偏移是-4),但汇编的分析则不是。我很可能是错的,但我找不到OP说Dwarf报告SP - 12的地方。 - Jose
DWARF将i的位置表示为相对于“帧基址”的偏移量。例如,在delay+0x18处,它比帧基址的当前值低12个字节。然而,对于这个PC,帧基址比r13的当前值高8个字节;实际上,对于OP的情况,帧基址是一个固定值,等于进入函数时的堆栈指针。因此,在delay+0x18处,DWARF给出了i的位置,即相对于r13原始值低12个字节,即比r13的当前值低4个字节。汇编显示,i比r13的当前值高4个字节。 - Robert Harris
你的结论是错误的:例如,在delay+0x18处,i的位置是(在delay+0x18处r13的值)+8-12=sp(0)-12=sp-4。 - Robert Harris

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接