Ferramentas do usuário

Ferramentas do site


dev:dissecando_programa_c

Dissecando um programa em C

Neste post quero tentar desmistificar o funcionando interno de um programa em C como o sistema operacional trabalha com os registradores da CPU, posições de memória, etc. Para isso vamos utilizar uma cobaia nascida especialmente para isso. Nosso código cobaia se chama cobaia.c e segue abaixo o código com ele que iremos trabalhar.

Todo o ambiente foi executado em cima do sistema operacional Linux e fez uso de diversos softwares:

  • gcc
  • gdb
  • vim
cobaia.c
int foo(int x, int y)
{
        int total;
        total=x+y;
        return 0;
}
 
 
int main()
{
        int a=10;
        int b=20;
        foo(a, b);
        return 0;
}

Primeiro item a ser feito e compilar nossa cobaia com opções de debug conseguimos isso com a flag de comando “-g” do compilador gcc.

#gcc -g -o cobaia.c cobaia

Depois de compilado vamos chamar o debugger gdb passando como parametro nossa cobaia.

ricardobarbosa@isadora:~$ gdb cobaia
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from cobaia...done.
warning: File "/home/ricardobarbosa/dev/c/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add.
        add-auto-load-safe-path /home/ricardobarbosa/dev/c/.gdbinit
line to your configuration file "/home/ricardobarbosa/.gdbinit".
To completely disable this security protection add
        set auto-load safe-path /
line to your configuration file "/home/ricardobarbosa/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
        info "(gdb)Auto-loading safe path"
>>> 

Vamos marcar um breakpoint na função main e executar dentro do nosso debugger.

>>> break main
Breakpoint 1 at 0x400511: file cobaia.c, line 11.
>>> run

Após executar veremos a seguinte tela. A tela do seu teste muito provavelmente será diferente porque eu utilizo um gdb dashboard que fica o tempo todo me mostrando os valores de registrador, memória, código fonte, saída de comandos, etc. Eu vou continuar esse tutorial como se estivessemos utilizando o gdb cru.

Bom nossa cobaia esta rodando vamos agora verificar alguns items e anotar, para entendimento futuro, peço que tenha profunda atenção porque para mim foi dificil entender e se perder em endereços de memória é muito fácil :) A não ser que você seja um gênio, entretanto nesse caso creio que você não estaria lendo este artigo desse pobre plebeu :).

Vamos anotar os seguintes dados

Endereço da memória onde esta a função main 0x400509
Endereço da memória onde esta a função foo 0x4004ed
Valor do registrador SP 0x00007FFFFFFFDBE0
Valor do registrador BP 0x00007FFFFFFFDBF0

E como obtemos os endereços de memória? com os seguintes comandos

Função main

>>> p &main
$1 = (int (*)()) 0x400509 <main>
>>> 

Obs: Quem já programou em C e com ponteiros, sabe que o operador “&“ indica posição de memória, o mesmo vale aqui para o gdb.

Função foo

>>> p &foo
$2 = (int (*)(int, int)) 0x4004ed <foo>
>>> 

Registradores

>>> info registers
rax            0x400509 4195593
rbx            0x0      0
rcx            0x0      0                                                                                                                                                           
rdx            0x7fffffffdce8   140737488346344                                                                                                                                     
rsi            0x7fffffffdcd8   140737488346328                                                                                                                                     
rdi            0x1      1                                                                                                                                                           
rbp            0x7fffffffdbf0   0x7fffffffdbf0                                                                                                                                      
rsp            0x7fffffffdbe0   0x7fffffffdbe0                                                                                                                                      
r8             0x7ffff7dd4e80   140737351863936                                                                                                                                     
r9             0x7ffff7dea530   140737351951664                                                                                                                                     
r10            0x7fffffffda80   140737488345728                                                                                                                                     
r11            0x7ffff7a36e50   140737348070992                                                                                                                                     
r12            0x400400 4195328                                                                                                                                                     
r13            0x7fffffffdcd0   140737488346320                                                                                                                                     
r14            0x0      0                                                                                                                                                           
r15            0x0      0                                                                                                                                                           
rip            0x400511 0x400511 <main+8>                                                                                                                                           
eflags         0x202    [ IF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0
>>> 

Agora vamos visualizar o conteudo da pilha ou nossa stack, mas vou usar o termo pilha. Para isso utilizamos o comando x do gdb(eu desconfio que o x seja de eXamine)

>>> x/32x 0x7fffffffdbe0
0x7fffffffdbe0: 0xffffdcd0      0x00007fff      0x00000000      0x00000000
0x7fffffffdbf0: 0x00000000      0x00000000      0xf7a36f45      0x00007fff
0x7fffffffdc00: 0x00000000      0x00000000      0xffffdcd8      0x00007fff
0x7fffffffdc10: 0x00000000      0x00000001      0x00400509      0x00000000
0x7fffffffdc20: 0x00000000      0x00000000      0x19bde97a      0xeda1a81f
0x7fffffffdc30: 0x00400400      0x00000000      0xffffdcd0      0x00007fff
0x7fffffffdc40: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdc50: 0xa1bde97a      0x125e57e0      0xc447e97a      0x125e4759
>>> 

Para entendimento vamos omitir a porção 0x7fffffff e iremos trabalhar com o resto. Sendo assim de DBE0 até DBF0 temos 16 posições porque os valores possíveis de hexa são: 0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f. Assim temos as seguintes posições de memória

  • DBE0
  • DBE1
  • DBE2
  • DBE3
  • DBE4
  • DBE5
  • DBE6
  • DBE7
  • DBE8
  • DBE9
  • DBEA
  • DBEB
  • DBEC
  • DBED
  • DBEE
  • DBEF
  • DBF0

Repare que na saída do comando x foi omitido alguns valores, visto que esses valores estao no intervalo. Para entendermos melhor vamos analisar a seguinte linha da saída do comando x.

0x7fffffffdbe0: 0xffffdcd0      0x00007fff      0x00000000      0x00000000
endereço de memória valor de 16bits valor de 16bits valor de 16bits valor de 16bits
0x7fffffffdbe0: 0xffffdcd0 0x00007fff 0x00000000 0x00000000

Para ficar melhor vamos visualizar em binário.

>>> x/32t 0x7fffffffdbe0
0x7fffffffdbe0: 11111111111111111101110011010000        00000000000000000111111111111111        00000000000000000000000000000000        00000000000000000000000000000000
0x7fffffffdbf0: 00000000000000000000000000000000        00000000000000000000000000000000        11110111101000110110111101000101        00000000000000000111111111111111
0x7fffffffdc00: 00000000000000000000000000000000        00000000000000000000000000000000        11111111111111111101110011011000        00000000000000000111111111111111
0x7fffffffdc10: 00000000000000000000000000000000        00000000000000000000000000000001        00000000010000000000010100001001        00000000000000000000000000000000
0x7fffffffdc20: 00000000000000000000000000000000        00000000000000000000000000000000        00011001101111011110100101111010        11101101101000011010100000011111
0x7fffffffdc30: 00000000010000000000010000000000        00000000000000000000000000000000        11111111111111111101110011010000        00000000000000000111111111111111
0x7fffffffdc40: 00000000000000000000000000000000        00000000000000000000000000000000        00000000000000000000000000000000        00000000000000000000000000000000
0x7fffffffdc50: 10100001101111011110100101111010        00010010010111100101011111100000        11000100010001111110100101111010        00010010010111100100011101011001
>>> 

Fico meio grande para contar a quantidade de bits né? vamos quebrar em porção de 8bits

>>> x/32tb 0x7fffffffdbe0
0x7fffffffdbe0: 11010000        11011100        11111111        11111111        11111111        01111111        00000000        00000000
0x7fffffffdbe8: 00000000        00000000        00000000        00000000        00000000        00000000        00000000        00000000
0x7fffffffdbf0: 00000000        00000000        00000000        00000000        00000000        00000000        00000000        00000000
0x7fffffffdbf8: 01000101        01101111        10100011        11110111        11111111        01111111        00000000        00000000
>>> 

São 4 colunas de 32 bits ou ainda 8 colunas de 8 bits. Cada posição de memória armazena 8 bits assim a primeira posição é DBE0 conta 8 DBE8.

Bom estamos visualizando a pilha e vamos executar dois passos que são as instruções:

int a=10;
int b=20;

Para executar um passo de cada vez utilizamos o comando “next”.

Vamos examinar novamente a memória a partir do valor do registrador BP? que no caso e nossa base da pilha é a pilha e um LIFO(Last IN Fisrt Out). Repare na linha marcada! estamos trabalhando com variavéis do tipo int, um int ocupa 4bytes(4×8=32) porque a posição de memória guarda 8 bits.

>>> x/32x 0x7fffffffdbe0
0x7fffffffdbe0: 0xd0    0xdc    0xff    0xff    0xff    0x7f    0x00    0x00
0x7fffffffdbe8: 0x0a    0x00    0x00    0x00    0x14    0x00    0x00    0x00
0x7fffffffdbf0: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x7fffffffdbf8: 0x45    0x6f    0xa3    0xf7    0xff    0x7f    0x00    0x00
>>> 

O armazenamento na memória neste caso e utilizado o método little-endian. Segue um endereço que considero bem explicado https://pt.stackoverflow.com/questions/50457/o-que-%C3%A9-big-endian-e-qual-a-diferen%C3%A7a-para-little-endian

Então temos o conteúdo da variável “a” como

0x0a    0x00    0x00    0x00

Isso e little-endian ficaria como 0x0000000a valor em hexadecimal que convertido para decimal seria o nosso valor 10. Vamos a próxima porção

0x14    0x00    0x00    0x00

Em little-endian seria em hexadecimal 0x00000014 que em decimal é igual a 20. Pode pegar sua calculadora e comprovar, ou melhor vamos visualizar em blocos de 32 bits.

>>> x/32xw 0x7fffffffdbe0
0x7fffffffdbe0: 0xffffdcd0      0x00007fff      0x0000000a      0x00000014
0x7fffffffdbf0: 0x00000000      0x00000000      0xf7a36f45      0x00007fff
0x7fffffffdc00: 0x00000000      0x00000000      0xffffdcd8      0x00007fff
0x7fffffffdc10: 0x00000000      0x00000001      0x00400509      0x00000000
0x7fffffffdc20: 0x00000000      0x00000000      0x19bde97a      0xeda1a81f
0x7fffffffdc30: 0x00400400      0x00000000      0xffffdcd0      0x00007fff
0x7fffffffdc40: 0x00000000      0x00000000      0x00000000      0x00000000
0x7fffffffdc50: 0xa1bde97a      0x125e57e0      0xc447e97a      0x125e4759
>>> 

Continuando nossa jornada vamos para a próxima instrução, executar a função foo. Se digitarmos next ele executara e mostrará apenas as atualizações de registradores e resultado, ao invés disso vamos entrar dentro da função e executar passo a passo, fazemos isso com o comando step ou s.

Quando executamos o comando step, vamos olhar o valor dos registradores SP e BP

>>> info registers rsp
rsp            0x7fffffffdbd0   0x7fffffffdbd0
>>> info registers rbp
rbp            0x7fffffffdbd0   0x7fffffffdbd0
>>> 

Note que temos outra pilha, note que este valor fica para cima do valor inicial 0x00007FFFFFFFDBF0, ou seja, DBF0 e maior que DBD0.

Vamos consultar a nova pilha ou sub-pilha?, mas antes vamos verificar o valor do endereço onde esta a variavel total.

>>> p &total
$1 = (int *) 0x7fffffffdbcc
>>> 

O valor DBCC esta acima do valor da base da pilha. Vamos verificar a pilha a partir da posicao da variavel total.

>>> x/32x 0x7fffffffdbcc
0x7fffffffdbcc: 0x00000000      0xffffdbf0      0x00007fff      0x0040052e
0x7fffffffdbdc: 0x00000000      0xffffdcd0      0x00007fff      0x0000000a
0x7fffffffdbec: 0x00000014      0x00000000      0x00000000      0xf7a36f45
0x7fffffffdbfc: 0x00007fff      0x00000000      0x00000000      0xffffdcd8
0x7fffffffdc0c: 0x00007fff      0x00000000      0x00000001      0x00400509
0x7fffffffdc1c: 0x00000000      0x00000000      0x00000000      0xc9c883cb
0x7fffffffdc2c: 0x6fe76531      0x00400400      0x00000000      0xffffdcd0
0x7fffffffdc3c: 0x00007fff      0x00000000      0x00000000      0x00000000
>>> 

Repare uma coisa na seguência.

>>> x/32x 0x7fffffffdbcc
0x7fffffffdbcc: 0x00000000 0xffffdbf0 0x00007fff 0x0040052e
0x7fffffffdbdc: 0x00000000 0xffffdcd0 0x00007fff 0x0000000a
0x7fffffffdbec: 0x00000014
  • 0x00000000: Posição para variavel total
  • 0xffffdbf0: Valor do registrador BP antigo antes de entrar na função foo
  • 0x0040052e: Endereço de retorno

O endereço 0x0040052e é o endereço de retorno para continuar na função main

>>> disassemble main
Dump of assembler code for function main:
   0x0000000000400509 <+0>:     push   %rbp
   0x000000000040050a <+1>:     mov    %rsp,%rbp
   0x000000000040050d <+4>:     sub    $0x10,%rsp
   0x0000000000400511 <+8>:     movl   $0xa,-0x8(%rbp)
   0x0000000000400518 <+15>:    movl   $0x14,-0x4(%rbp)
   0x000000000040051f <+22>:    mov    -0x4(%rbp),%edx
   0x0000000000400522 <+25>:    mov    -0x8(%rbp),%eax
   0x0000000000400525 <+28>:    mov    %edx,%esi
   0x0000000000400527 <+30>:    mov    %eax,%edi
   0x0000000000400529 <+32>:    callq  0x4004ed <foo>
   0x000000000040052e <+37>:    mov    $0x0,%eax
   0x0000000000400533 <+42>:    leaveq 
   0x0000000000400534 <+43>:    retq   
End of assembler dump.
>>> 

Vamos continuar executando “next” e depois visualizar o conteúdo da memória

>>> x/32xw 0x7fffffffdbcc
0x7fffffffdbcc: 0x0000001e      0xffffdbf0      0x00007fff      0x0040052e
0x7fffffffdbdc: 0x00000000      0xffffdcd0      0x00007fff      0x0000000a
0x7fffffffdbec: 0x00000014      0x00000000      0x00000000      0xf7a36f45
0x7fffffffdbfc: 0x00007fff      0x00000000      0x00000000      0xffffdcd8
0x7fffffffdc0c: 0x00007fff      0x00000000      0x00000001      0x00400509
0x7fffffffdc1c: 0x00000000      0x00000000      0x00000000      0xc9c883cb
0x7fffffffdc2c: 0x6fe76531      0x00400400      0x00000000      0xffffdcd0
0x7fffffffdc3c: 0x00007fff      0x00000000      0x00000000      0x00000000
>>> 

Note o valor da posição de memória 0x7fffffffdbcc antes era 0x00000000 e agora é 0x0000001e, 1E em hexadecimal é igual ao valor 30 em decimal que seria a soma da variável 10 + 30.

Continuando. Vamos disassemblar a função foo para ver onde estamos executando.

>>> disassemble foo
Dump of assembler code for function foo:
   0x00000000004004ed <+0>:     push   %rbp
   0x00000000004004ee <+1>:     mov    %rsp,%rbp
   0x00000000004004f1 <+4>:     mov    %edi,-0x14(%rbp)
   0x00000000004004f4 <+7>:     mov    %esi,-0x18(%rbp)
   0x00000000004004f7 <+10>:    mov    -0x18(%rbp),%eax
   0x00000000004004fa <+13>:    mov    -0x14(%rbp),%edx
   0x00000000004004fd <+16>:    add    %edx,%eax
   0x00000000004004ff <+18>:    mov    %eax,-0x4(%rbp)
   0x0000000000400502 <+21>:    mov    $0x0,%eax
=> 0x0000000000400507 <+26>:    pop    %rbp
   0x0000000000400508 <+27>:    retq   
End of assembler dump.
>>> 

A instrução “pop %rbp” remove da pilha jogando no registrador bp(base pointer, base da pilha). Restauramos a pilha com o valor 0xffffdbf0, e depois retornamos para o endereço 0x0040052e que disassemblando a função main veremos qual posição é.

>>> disassemble main
Dump of assembler code for function main:
   0x0000000000400509 <+0>:     push   %rbp
   0x000000000040050a <+1>:     mov    %rsp,%rbp
   0x000000000040050d <+4>:     sub    $0x10,%rsp
   0x0000000000400511 <+8>:     movl   $0xa,-0x8(%rbp)
   0x0000000000400518 <+15>:    movl   $0x14,-0x4(%rbp)
   0x000000000040051f <+22>:    mov    -0x4(%rbp),%edx
   0x0000000000400522 <+25>:    mov    -0x8(%rbp),%eax
   0x0000000000400525 <+28>:    mov    %edx,%esi
   0x0000000000400527 <+30>:    mov    %eax,%edi
   0x0000000000400529 <+32>:    callq  0x4004ed <foo>
   0x000000000040052e <+37>:    mov    $0x0,%eax
   0x0000000000400533 <+42>:    leaveq 
   0x0000000000400534 <+43>:    retq   
End of assembler dump.
>>> 

A posição 0x0040052e é a posição abaixo da chamada a função foo. Depois que executamos o comando next restauramos a pilha antiga.

>>> info registers $rbp
rbp            0x7fffffffdbd0   0x7fffffffdbd0
>>> next
>>> info registers $rbp
rbp            0x7fffffffdbf0   0x7fffffffdbf0
>>> 

Voltamos para função main e praticamente terminamos a execução da nossa cobaia, ele executa um mov zerando o registrador eax e executando a instrução leaveq, que configura a pilha, primeiro colocando dentro do registrador bp o valor inicial e copiando SP para dentro do registrador BP, e a instrução retq, coloca o endereço de retorno da pilha em rip(registrador contador de programa, retomando assim o endereço de retorno salvo.

Finalizando a nossa cobaia. Espero que com esse tutorial conseguimos dissecar nossa cobaia e qualquer dúvida ou dados a acrescentar comente ou mailme.

Att.