Análise de Stack Frames sem o Registrador RBP Utilizando GDB e DWARF

Em compilações x86_64 modernas e otimizadas, os compiladores frequentemente aplicam a flag -fomit-frame-pointer por padrão. Essa otimização libera o registrador rbp (ou ebp em arquiteturas de 32 bits) para ser utilizado como um registrador de propósito geral. Consequentemente, o prólogo tradicional das funções, que configura o ponteiro de base (push %rbp; mov %rsp, %rbp), é completamente omitido.

Sem uma cadeia de ponteiros de frame explícita na memória, a navegação pela pilha de chamadas (call stack) passa a depender integralmente das metainformações de depuração. O depurador GDB contorna essa limitação utilizando as Call Frame Information (CFI) embutidas no formato DWARF, rastreando as variáveis e endereços de retorno através do Canonical Frame Address (CFA).

Estrutura de Memória da Pilha

Sem o rbp atuando como âncora, um stack frame inicia-se diretamente com o endereço de retorno da função chamadora, seguido pelo espaço alocado para as variáveis locais através da manipulação direta do stack pointer (rsp). Abaixo, observa-se um dump de memória demonstrando essa disposição para uma cadeia de chamadas envolvendo as funções worker_task, start_task, compute e log_output:

0x7fffffffe058: 0x004011a0 0x00000000 0x00000000 0x00000000 <----- Stack frame de worker_task [0x7fffffffe060, 0x7fffffffe080], onde x=0x2b. O endereço base contém o retorno para 0x4011c5.
0x7fffffffe068: 0x00000000 0x0000002b 0x00000000 0x00000000  O valor 0x4011c5 é o endereço de retorno para start_task.
0x7fffffffe078: 0x004011c5 0x00000000 0x00401050 0x00000000  
0x7fffffffe088: 0xffffee00 0x0000002a 0x00000000 0x00000000 <----- Stack frame de start_task [0x7fffffffe080, 0x7fffffffe0a0], onde x=0x2a.
0x7fffffffe098: 0x004012f0 0x00000000 0x00000000 0x00000000 <----- Stack frame de compute [0x7fffffffe0a0, 0x7fffffffe0d0], onde x=0x29, y=0x3c.
0x7fffffffe0a8: 0x0000003c 0x00000029 0x00000001 0x00000000   
0x7fffffffe0b8: 0x00000050 0x00000000 0x826eb026 0x00000000  
0x7fffffffe0c8: 0x004013a0 0x00000000 0xffffee60 0x00007fff  
0x7fffffffe0d8: 0xf7dee923 0x00000002 0x00000000 0x00000017  
0x7fffffffe0e8: 0x0000003c 0x00000000 0x00000000 0x00000000  

Interpretação dos Frames pelo GDB

Ao inspecionar a pilha, o GDB identifica o endereço base de cada frame (o CFA). O output do comando info frame demonstra como o depurador mapeia os argumentos e variáveis locais sem depender do registrador rbp:

(gdb) info frame 1
Stack frame at 0x7fffffffe080:
 rip = 0x4011a0 in worker_task (tasks.c:22); saved rip = 0x4011c5
 called by frame at 0x7fffffffe0a0, caller of frame at 0x7fffffffe060
 source language c.
 Arglist at 0x7fffffffe058, args: x=43
 Locals at 0x7fffffffe058, Previous frame's sp is 0x7fffffffe080
 Saved registers:
  rip at 0x7fffffffe078
(gdb) info frame 2
Stack frame at 0x7fffffffe0a0:
 rip = 0x4011c5 in start_task (tasks.c:34); saved rip = 0x4012f0
 called by frame at 0x7fffffffe0d0, caller of frame at 0x7fffffffe080
 source language c.
 Arglist at 0x7fffffffe078, args: x=42
 Locals at 0x7fffffffe078, Previous frame's sp is 0x7fffffffe0a0
 Saved registers:
  rip at 0x7fffffffe098
(gdb) info frame 3
Stack frame at 0x7fffffffe0d0:
 rip = 0x4012f0 in compute (math_ops.c:15); saved rip = 0x4013a0
 called by frame at 0x7fffffffef00, caller of frame at 0x7fffffffe0a0
 source language c.
 Arglist at 0x7fffffffe098, args: x=41, y=60
 Locals at 0x7fffffffe098, Previous frame's sp is 0x7fffffffe0d0
 Saved registers:
  rip at 0x7fffffffe0c8
(gdb) info frame 4
Stack frame at 0x7fffffffef00:
 rip = 0x4013a0 in log_output (logger.c:48); saved rip = 0x4014b0
 called by frame at 0x7fffffffef10, caller of frame at 0x7fffffffe0d0
 source language c.
 Arglist at 0x7fffffffe0c8, args: level=2
 Locals at 0x7fffffffe0c8, Previous frame's sp is 0x7fffffffef00
 Saved registers:
  rip at 0x7fffffffef08

Análise do Código Assembly

O código de máquina gerado reflete a ausência do frame pointer. O espaço na pilha é reservado e liberado diretamente manipulando o registrador rsp, e os parâmetros recebidos via registradores (como edi) são descarregados (spilled) em offsets fixos em relação ao rsp atual.

0000000000401180 <worker_task>:
  401180:   48 83 ec 20             sub    $0x20,%rsp    
  401184:   89 7c 24 10             mov    %edi,0x10(%rsp)
  401188:   83 44 24 10 02          addl   $0x2,0x10(%rsp)
  40118d:   8b 44 24 10             mov    0x10(%rsp),%eax
  401191:   89 c7                   mov    %eax,%edi
  401193:   e8 b0 fe ff ff          callq  401048 <helper_func>
  401198:   90                      nop
  401199:   48 83 c4 20             add    $0x20,%rsp
  40119d:   c3                      retq   

00000000004011a0 <start_task>:
  4011a0:   48 83 ec 20             sub    $0x20,%rsp
  4011a4:   89 7c 24 10             mov    %edi,0x10(%rsp)
  4011a8:   83 44 24 10 02          addl   $0x2,0x10(%rsp)
  4011ad:   8b 44 24 10             mov    0x10(%rsp),%eax
  4011b1:   89 c7                   mov    %eax,%edi
  4011b3:   e8 c8 ff ff ff          callq  401180 <worker_task> 
  4011b8:   90                      nop
  4011b9:   48 83 c4 20             add    $0x20,%rsp
  4011bd:   c3                      retq

Mapeamento via DWARF CFI

Para que o GDB saiba que a variável x está localizada em 0x10(%rsp), o compilador injeta regras no objeto ELF. Através da diretiva DW_AT_frame_base, o depurador é instruído a calcular o CFA dinamicamente. Os parâmetros e variáveis locais são então indexados utilizando DW_OP_fbreg com um deslocamento negativo a partir desse endereço canônico.

< 1><0x000000d0>    DW_TAG_subprogram
                      DW_AT_external              yes(1)
                      DW_AT_name                  "worker_task"
                      DW_AT_decl_file             0x00000001 /src/core/tasks.c
                      DW_AT_decl_line             0x00000015
                      DW_AT_prototyped            yes(1)
                      DW_AT_type                  <0x00000055>
                      DW_AT_low_pc                0x00401180
                      DW_AT_high_pc               <offset-from-lowpc>30
                      DW_AT_frame_base            len 0x0001: 9c: DW_OP_call_frame_cfa
                      DW_AT_GNU_all_tail_call_sitesyes(1)
                      DW_AT_sibling               <0x00000110>
< 2><0x000000f0>      DW_TAG_formal_parameter
                        DW_AT_name                  "x"
                        DW_AT_decl_file             0x00000001 /src/core/tasks.c
                        DW_AT_decl_line             0x00000015
                        DW_AT_type                  <0x00000055>
                        DW_AT_location              len 0x0002: 9170: DW_OP_fbreg -16
< 1><0x00000110>    DW_TAG_subprogram
                      DW_AT_external              yes(1)
                      DW_AT_name                  "start_task"
                      DW_AT_decl_file             0x00000001 /src/core/tasks.c
                      DW_AT_decl_line             0x00000021
                      DW_AT_prototyped            yes(1)
                      DW_AT_type                  <0x00000055>
                      DW_AT_low_pc                0x004011a0
                      DW_AT_high_pc               <offset-from-lowpc>30
                      DW_AT_frame_base            len 0x0001: 9c: DW_OP_call_frame_cfa
                      DW_AT_GNU_all_tail_call_sitesyes(1)
                      DW_AT_sibling               <0x00000140>
< 2><0x00000130>      DW_TAG_formal_parameter
                        DW_AT_name                  "x"
                        DW_AT_decl_file             0x00000001 /src/core/tasks.c
                        DW_AT_decl_line             0x00000021
                        DW_AT_type                  <0x00000055>
                        DW_AT_location              len 0x0002: 9170: DW_OP_fbreg -16

Tags: gdb DWARF x86_64 assembly stack-frame

Publicado em 10-23 06:17