Локальное расположение переменных из информации DWARF в ARM

У меня есть программа C в файле delay.c:

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

Затем я скомпилирую программу с gcc 4.6.3 на эмуляторе ARM (армель, точнее) с помощью команды 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 функции от отладочной информации. Ниже представлена информация о delay и i в .debug_info разделе:

<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 между расчетом из отладочной информации и сбором?

Заранее спасибо!

Ответ 1

TL; DR. Анализ в вопросе правильный, и несоответствие является ошибкой в одном из компонентов gcc (GNU Arm Embedded Toolchain является очевидным местом для регистрации одного).

Как бы то ни было, этот другой ответ неверен, поскольку он ошибочно связывает значение указателя стека при оценке выражения местоположения с более ранним значением указателя стека при входе в функцию.

Что касается DWARF, расположение i зависит от счетчика программ. Рассмотрим, например, delay+0x18 текстового адреса delay+0x18. На этом этапе местоположение i задается DW_OP_fbreg(-12), то есть 12 байт ниже базы кадра. База кадра задается родительским атрибутом DW_TAG_subprogram DW_AT_frame_base который в этом случае также зависит от счетчика программы: для delay+0x18 его выражение равно DW_OP_breg13(8), то есть r13 + 8. Важно отметить, что этот расчет использует текущее значение r13, то есть значение r13 когда счетчик программ равен delay+0x18.

Таким образом, DWARF утверждает, что при delay+0x18 i находится на r13 + 8 - 12, т.е. на 4 байта ниже нижней части существующего стека. Проверка сборки показывает, что при delay+018 i должно быть найдено 4 байта над нижней частью стека. Поэтому 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 и 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, соответствующее сборке. Повторение эксперимента gdb с исправленным DWARF показывает ожидаемое значение 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

Я не согласен с анализом OP asm:

00000000 <delay>:                         ; so far, let 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, что соответствует анализу карликов (в котором говорится, что i находится в 0 + 8 - 12)

Изменить, чтобы добавить информацию о моем анализе DWARF:

В соответствии с этой строкой: 00000020 0000000c 00000020 (DW_OP_fbreg: -12), являющийся DW_OP_fbreg:

Операция DW_OP_fbreg предоставляет подписанное смещение LEB128 от адреса, указанного в описании местоположения, в атрибуте DW_AT_frame_base текущей функции. (Обычно это регистр "указатель стека" плюс или минус какое-то смещение. В более сложных системах это может быть список местоположений, который корректирует смещение в соответствии с изменениями указателя стека по мере изменения ПК).

адрес - frame_base + offset, где:

  • frame_base: указатель стека + / - некоторое смещение и согласно предыдущей строке (00000000 00000004 00000038 (DW_OP_breg13 (r13): 8)), с 00000004 до 00000038, он имеет смещение +8 (r13 - SP)
  • смещение: очевидно, -12

Учитывая, что DWARF указывает, что он указывает на sp (0) + 8 - 12 = sp(0) - 4