====== 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 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 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: . Find the GDB manual and other documentation resources online at: . 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. {{ :dev:dissecandoc.png?direct&400 |}} 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
>>> 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 >>> 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 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(4x8=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**. {{ :dev:dissecandoc1.png?direct&400 |}} 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 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 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.