Aula 03: Desmontando(Disassembling) um Programa
Conteúdo
- 1. Um programa binário … o que é?
- 2.
objdump
ereadelf
básico - 3. x86 o estado do Registro do Processador
- 3.1. x86 Conjunto de instruções
- 3.2. Anatomia de uma Instrução
- 3.3. Registradores do Processador
- 3.4. O ponteiro base e o ponteiro de pilha
- 3.5. Gerenciando o quadro de pilha e a pilha
- 3.6. Referenciando, desreferenciando e configurando a memória
- 3.7. Loops, Saltos e Testes de condição
- 3.8. Chamadas de Função
1 Um programa binário … o que é?
Analisamos muito como escrever programas em c, compilá-los em binários e, em seguida, executá-los. Agora vamos olhar diretamente para os próprios arquivos binários. Nós vamos examinar o que exatamente é um binário, como ele é formatado e como desmembra-lo ou fazer o dissambler do conteúdo dentro?
2 objdump
e readelf
básico
Para toda esta aula, vamos separar um programa helloworld simples:
#include <stdio.h> int main(int argc, char *argv){ char hello[15]="Hello, World!\n"; char * p; for(p = hello; *p; p++){ putchar(*p); } return 0; }
Vamos compilar o programa para criar um binário:
felipe@pc:~/binaryanalysis$ gcc helloworld.c -o helloworld
Agora, se usarmos o comando file
, podemos ver que tipo de arquivo o binário é.
felipe@pc:~/binaryanalysis$ file helloworld helloworld: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=2b27688b97f10f626f1ff62c232d7a2298d6afa1, not stripped
Vemos que na verdade é um arquivo ELF
, que significa Executable
and Linkable Format. Trabalharemos exclusivamente com binários em ELF.
2.1 ELF Files e ELF Headers
Todos os arquivos ELF têm um cabeçalho descrevendo as diferentes seções e Informações gerais. Podemos ler as informações do cabeçalho do nosso programa helloworld
usando o readelf
felipe@pc:~/binaryanalysis$ readelf -h helloworld ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048370 Start of program headers: 52 (bytes into file) Start of section headers: 4472 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 9 Size of section headers: 40 (bytes) Number of section headers: 30 Section header string table index: 27
A maioria dessas informações não é muito útil, mas deixe-me apontar algumas coisas importantes.
- Existe um número mágico! O número mágico é usado para dizer, ei, isso é ELF e qual versão
- A classe é ELF32, então é de 32 bits
- A máquina é Intel 80386, ou x386
- O ponto de entrada para o arquivo é o endereço 0x804870, essencialmente o que é a primeira instrução na função de seção _start que chama main.
Todo o resto não é super útil para nossos propósitos. Outra coisa interessante que podemos fazer com o readelf
é olhar para todas as seções, que são regiões do binário para diferentes propósitos.
felipe@pc:~/binaryanalysis$ readelf -S helloworld There are 30 section headers, starting at offset 0x1178: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4 [ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4 [ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4 [ 5] .dynsym DYNSYM 080481cc 0001cc 000060 10 A 6 1 4 [ 6] .dynstr STRTAB 0804822c 00022c 000068 00 A 0 0 1 [ 7] .gnu.version VERSYM 08048294 000294 00000c 02 A 5 0 2 [ 8] .gnu.version_r VERNEED 080482a0 0002a0 000030 00 A 6 1 4 [ 9] .rel.dyn REL 080482d0 0002d0 000008 08 A 5 0 4 [10] .rel.plt REL 080482d8 0002d8 000020 08 A 5 12 4 [11] .init PROGBITS 080482f8 0002f8 000023 00 AX 0 0 4 [12] .plt PROGBITS 08048320 000320 000050 04 AX 0 0 16 [13] .text PROGBITS 08048370 000370 0001f2 00 AX 0 0 16 [14] .fini PROGBITS 08048564 000564 000014 00 AX 0 0 4 [15] .rodata PROGBITS 08048578 000578 000008 00 A 0 0 4 [16] .eh_frame_hdr PROGBITS 08048580 000580 00002c 00 A 0 0 4 [17] .eh_frame PROGBITS 080485ac 0005ac 0000b0 00 A 0 0 4 [18] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4 [19] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4 [20] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4 [21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4 [22] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4 [23] .got.plt PROGBITS 0804a000 001000 00001c 04 WA 0 0 4 [24] .data PROGBITS 0804a01c 00101c 000008 00 WA 0 0 4 [25] .bss NOBITS 0804a024 001024 000004 00 WA 0 0 1 [26] .comment PROGBITS 00000000 001024 00004d 01 MS 0 0 1 [27] .shstrtab STRTAB 00000000 001071 000106 00 0 0 1 [28] .symtab SYMTAB 00000000 001628 000440 10 29 45 4 [29] .strtab STRTAB 00000000 001a68 000274 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
Novamente, um monte dessas informações não é muito útil para nós, mas pode ser mais tarde. Algumas coisas importantes a serem observadas:
- A seção .bss está listada, isso é o mesmo que bss no layout da memória do programa
- Há também a seção .data, igual ao layout da memória do programa
- Finalmente, há uma seção .text, igual a antes. E observe que é no endereço 0x08048370 que é o mesmo endereço no cabeçalho para o início(_start) das instruções do programa.
2.2 Chegando à montagem(assembly) com objdump
Agora que temos uma ideia de como o arquivo é formatado, seria bom entrar nos detalhes das instruções da máquina em si. Para isso, usaremos objdump
ou "object dump". Simplesmente, nós podemos chamar o executável binário assim:
felipe@pc:~/binaryanalysis$ objdump -d helloworld helloworld: file format elf32-i386 Disassembly of section .init: 080482f8 <_init>: 80482f8: 53 push %ebx 80482f9: 83 ec 08 sub $0x8,%esp 80482fc: e8 9f 00 00 00 call 80483a0 <__x86.get_pc_thunk.bx> 8048301: 81 c3 ff 1c 00 00 add $0x1cff,%ebx 8048307: 8b 83 fc ff ff ff mov -0x4(%ebx),%eax 804830d: 85 c0 test %eax,%eax 804830f: 74 05 je 8048316 <_init+0x1e> 8048311: e8 2a 00 00 00 call 8048340 <__gmon_start__@plt> 8048316: 83 c4 08 add $0x8,%esp 8048319: 5b pop %ebx 804831a: c3 ret Disassembly of section .plt: 08048320 <__stack_chk_fail@plt-0x10>: 8048320: ff 35 04 a0 04 08 pushl 0x804a004 8048326: ff 25 08 a0 04 08 jmp *0x804a008 804832c: 00 00 add %al,(%eax) ...
Vai despejar um monte de coisas, mas vamos olhar com mais cuidado para baixo, Veremos um cabeçalho que parece familiar, main:
0804841d <main>: 804841d: 55 push %ebp 804841e: 89 e5 mov %esp,%ebp 8048420: 83 e4 f0 and $0xfffffff0,%esp 8048423: 83 ec 30 sub $0x30,%esp 8048426: c7 44 24 1d 48 65 6c movl $0x6c6c6548,0x1d(%esp) 804842d: 6c 804842e: c7 44 24 21 6f 2c 20 movl $0x57202c6f,0x21(%esp) 8048435: 57 8048436: c7 44 24 25 6f 72 6c movl $0x646c726f,0x25(%esp) 804843d: 64 804843e: 66 c7 44 24 29 21 0a movw $0xa21,0x29(%esp) 8048445: c6 44 24 2b 00 movb $0x0,0x2b(%esp) 804844a: 8d 44 24 1d lea 0x1d(%esp),%eax 804844e: 89 44 24 2c mov %eax,0x2c(%esp) 8048452: eb 17 jmp 804846b <main+0x4e> 8048454: 8b 44 24 2c mov 0x2c(%esp),%eax 8048458: 0f b6 00 movzbl (%eax),%eax 804845b: 0f be c0 movsbl %al,%eax 804845e: 89 04 24 mov %eax,(%esp) 8048461: e8 aa fe ff ff call 8048310 <putchar@plt> 8048466: 83 44 24 2c 01 addl $0x1,0x2c(%esp) 804846b: 8b 44 24 2c mov 0x2c(%esp),%eax 804846f: 0f b6 00 movzbl (%eax),%eax 8048472: 84 c0 test %al,%al 8048474: 75 de jne 8048454 <main+0x37> 8048476: b8 00 00 00 00 mov $0x0,%eax 804847b: c9 leave 804847c: c3 ret 804847d: 66 90 xchg %ax,%ax 804847f: 90 nop
Este é o assembly para a função principal. Olhando para o outro lado, da esquerda à direita, mais à esquerda está o endereço que esta instrução é carregado, então os bytes reais da instrução e, em seguida, finalmente o nome dos detalhes da instrução.
A primeira coisa que você pode notar sobre a instrução em si é que é muito, muito difícil de ler. Isso porque é a sintaxe da AT&T, o que, simplesmente, é uma merda! Usaremos um formato alternativo chamado Intel sintaxe, que é muito, muito melhor. Para isso, precisamos passar por um argumento para objdump
:
felipe@pc:~/binaryanalysis$ objdump -M intel -d helloworld helloworld: file format elf32-i386 Disassembly of section .init: 080482f8 <_init>: 80482f8: 53 push ebx 80482f9: 83 ec 08 sub esp,0x8 (... snip ...) 0804841d <main>: 804841d: 55 push ebp 804841e: 89 e5 mov ebp,esp 8048420: 83 e4 f0 and esp,0xfffffff0 8048423: 83 ec 30 sub esp,0x30 8048426: c7 44 24 1d 48 65 6c mov DWORD PTR [esp+0x1d],0x6c6c6548 804842d: 6c 804842e: c7 44 24 21 6f 2c 20 mov DWORD PTR [esp+0x21],0x57202c6f 8048435: 57 8048436: c7 44 24 25 6f 72 6c mov DWORD PTR [esp+0x25],0x646c726f 804843d: 64 804843e: 66 c7 44 24 29 21 0a mov WORD PTR [esp+0x29],0xa21 8048445: c6 44 24 2b 00 mov BYTE PTR [esp+0x2b],0x0 804844a: 8d 44 24 1d lea eax,[esp+0x1d] 804844e: 89 44 24 2c mov DWORD PTR [esp+0x2c],eax 8048452: eb 17 jmp 804846b <main+0x4e> 8048454: 8b 44 24 2c mov eax,DWORD PTR [esp+0x2c] 8048458: 0f b6 00 movzx eax,BYTE PTR [eax] 804845b: 0f be c0 movsx eax,al 804845e: 89 04 24 mov DWORD PTR [esp],eax 8048461: e8 aa fe ff ff call 8048310 <putchar@plt> 8048466: 83 44 24 2c 01 add DWORD PTR [esp+0x2c],0x1 804846b: 8b 44 24 2c mov eax,DWORD PTR [esp+0x2c] 804846f: 0f b6 00 movzx eax,BYTE PTR [eax] 8048472: 84 c0 test al,al 8048474: 75 de jne 8048454 <main+0x37> 8048476: b8 00 00 00 00 mov eax,0x0 804847b: c9 leave 804847c: c3 ret 804847d: 66 90 xchg ax,ax 804847f: 90 nop 0804846d <main>: 804846d: 55 push ebp 804846e: 89 e5 mov ebp,esp 8048470: 83 e4 f0 and esp,0xfffffff0 8048473: 83 ec 30 sub esp,0x30 8048476: 65 a1 14 00 00 00 mov eax,gs:0x14 804847c: 89 44 24 2c mov DWORD PTR [esp+0x2c],eax 8048480: 31 c0 xor eax,eax 8048482: c7 44 24 1d 48 65 6c mov DWORD PTR [esp+0x1d],0x6c6c6548 8048489: 6c 804848a: c7 44 24 21 6f 2c 20 mov DWORD PTR [esp+0x21],0x57202c6f 8048491: 57 8048492: c7 44 24 25 6f 72 6c mov DWORD PTR [esp+0x25],0x646c726f 8048499: 64 804849a: 66 c7 44 24 29 21 0a mov WORD PTR [esp+0x29],0xa21 80484a1: c6 44 24 2b 00 mov BYTE PTR [esp+0x2b],0x0 80484a6: 8d 44 24 1d lea eax,[esp+0x1d] 80484aa: 89 44 24 18 mov DWORD PTR [esp+0x18],eax 80484ae: eb 17 jmp 80484c7 <main+0x5a> 80484b0: 8b 44 24 18 mov eax,DWORD PTR [esp+0x18] 80484b4: 0f b6 00 movzx eax,BYTE PTR [eax] 80484b7: 0f be c0 movsx eax,al 80484ba: 89 04 24 mov DWORD PTR [esp],eax 80484bd: e8 9e fe ff ff call 8048360 <putchar@plt> 80484c2: 83 44 24 18 01 add DWORD PTR [esp+0x18],0x1 80484c7: 8b 44 24 18 mov eax,DWORD PTR [esp+0x18] 80484cb: 0f b6 00 movzx eax,BYTE PTR [eax] 80484ce: 84 c0 test al,al 80484d0: 75 de jne 80484b0 <main+0x43> 80484d2: b8 00 00 00 00 mov eax,0x0 80484d7: 8b 54 24 2c mov edx,DWORD PTR [esp+0x2c] 80484db: 65 33 15 14 00 00 00 xor edx,DWORD PTR gs:0x14 80484e2: 74 05 je 80484e9 <main+0x7c> 80484e4: e8 47 fe ff ff call 8048330 <__stack_chk_fail@plt> 80484e9: c9 leave 80484ea: c3 ret 80484eb: 66 90 xchg ax,ax 80484ed: 66 90 xchg ax,ax 80484ef: 90 nop (... snip ...)
2.3 Dissasembling com gdb
Outra maneira de obter o código dissambly é usando gdb
, o gnu
debugger, que também faz uma série de outras tarefas que veremos posterior. Para começar, execute o programa no depurador:
felipe@pc:~/binaryanalysis$ gdb helloworld 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 "i686-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 helloworld...(no debugging symbols found)...done. (gdb)
Isso imprimirá um aviso de isenção de responsabilidade e deixará você em um terminal gdb. Agora Você pode digitar para desmontar a função principal disassemble main
. Para facilitar você pode digitar "alias ds=disassemble".
(gdb) ds main Dump of assembler code for function main: 0x0804841d <+0>: push ebp 0x0804841e <+1>: mov ebp,esp 0x08048420 <+3>: and esp,0xfffffff0 0x08048423 <+6>: sub esp,0x30 0x08048426 <+9>: mov DWORD PTR [esp+0x1d],0x6c6c6548 0x0804842e <+17>: mov DWORD PTR [esp+0x21],0x57202c6f 0x08048436 <+25>: mov DWORD PTR [esp+0x25],0x646c726f 0x0804843e <+33>: mov WORD PTR [esp+0x29],0xa21 0x08048445 <+40>: mov BYTE PTR [esp+0x2b],0x0 0x0804844a <+45>: lea eax,[esp+0x1d] 0x0804844e <+49>: mov DWORD PTR [esp+0x2c],eax 0x08048452 <+53>: jmp 0x804846b <main+78> 0x08048454 <+55>: mov eax,DWORD PTR [esp+0x2c] 0x08048458 <+59>: movzx eax,BYTE PTR [eax] 0x0804845b <+62>: movsx eax,al 0x0804845e <+65>: mov DWORD PTR [esp],eax 0x08048461 <+68>: call 0x8048310 <putchar@plt> 0x08048466 <+73>: add DWORD PTR [esp+0x2c],0x1 0x0804846b <+78>: mov eax,DWORD PTR [esp+0x2c] 0x0804846f <+82>: movzx eax,BYTE PTR [eax] 0x08048472 <+85>: test al,al 0x08048474 <+87>: jne 0x8048454 <main+55> 0x08048476 <+89>: mov eax,0x0 0x0804847b <+94>: leave 0x0804847c <+95>: ret End of assembler dump.
Se a saída estiver na sintaxe AT&T, então execute esse comando:
(gdb) set disassembly-flavor intel
Para ter a saída gdb na sintaxe Intel.
Vou trabalhar principalmente com a saída gdb dissambled porque é mais bem formatado. Nossa próxima tarefa é entender o que diabos está acontecendo?!?!
3 x86 o estado do Registro do Processador
3.1 x86 Conjunto de instruções
Vamos começar com um item simples, o que é x86? É um conjunto de instruções de montagem(assembly) – uma linguagem de programação. Podemos reprimir termos x86 de seu byte (como visto na saída objdump) ou em um formato legível humano (como visto na sintaxe Intel ou AT&T).
Você pode ter trabalhado com um conjunto de instruções anteriormente, como MIPS. MIPS tem a propriedade de ser um conjunto de instrumentos RISC, ou Reduce Instruction Set Computing, que tem a vantagem de que todos instruções e argumentos são sempre do mesmo tamanho, 32 bits.
x86 é um conjunto de instruções CISC, ou Complex Instruction Set Computing, e tem a propriedade de que os tamanhos das instruções não são contestados. Eles pode variar 8 bits e 64 bits e mais, dependendo do instrução. Você pode se perguntar, por que diabos qualquer coisa seria projetado desta forma? A resposta é a inércia do mercado e a capacidade de retrocesso. À medida que os chips Intel dominavam o mercado, mais e mais binários era x86.
Hoje, outro conjunto de instruções tornou-se muito relevante: ARM ou Acron Risc Machine. E, como o nome indica, é um conjunto de instruções RISC e, portanto, está trazendo de volta um pouco de sanidade ao mundo do conjunto de instruções. O ARM também é a arquitetura preferida em muitos dispositivos móveis, Portanto, será relevante por algum tempo.
No entanto, não trabalharemos com o ARM nesta classe, apenas x86 e usaremos apenas um conjunto muito pequeno de instruções x86.
3.2 Anatomia de uma Instrução
Uma instrução, no formato legível por humanos, tem o seguinte formato:
operation <dst>, <src>
O nome da operação é o tipo de operação que será realizada. Por exemplo, pode ser add
ou mov
ou and
. O
<dst>
é onde o resultado será armazenado, o que normalmente é um Registro. O <src>
é de onde os dados são lidos para serem operados sobre o qual também pode incluir dados referenciados no <dst>
. O
<src>
é opcional e depende do comando.
Se pegarmos algumas operações do nosso código de exemplo:
0x0804841d <+0>: push ebp 0x0804841e <+1>: mov ebp,esp 0x08048420 <+3>: and esp,0xfffffff0
O primeiro comando push
pega um argumento e coloca esse argumento em a pilha, ajustando o ponteiro da pilha. Nesse caso, ele envia o valor do ponteiro base armazenado no registro no pilha ebp
. O segundo comando recebe dois argumentos e mov
um valor de um local para outro, muito parecido com a atribuição. O segundo move o valor no ponteiro da pilha esp
e o salva no comando ponteiro base ebp
. Finalmente, o último comando é um bit a bit e operação tem dois argumentos. Ele executa um bitwise no
<dst>
com o <src>
e armazena o resultado em <dst>
. Nesse contexto, o comando and
alinha o ponteiro da pilha com o menor Valor de 4 bits. O alinhamento de 4 bits é devido a um bug antigo na divisão unidade do processador x86, e então você verá muito essa sequência em assembly.
Vamos dar uma olhada mais de perto nessas instruções novamente em um segundo, Mas antes de fazermos isso, precisamos entender esses registros e como eles são usados com mais detalhes.
3.3 Registradores do Processador
Os registros são espaços de armazenamento especiais no processador que armazenam o estado do programa. Alguns registros são usados para uso geral de armazenamento para armazenar um armazenamento intermediário, enquanto outros são usados para manter a noção do estado de execução, por exemplo, como qual é a próxima instrução.
Aqui estão os registros padrão que você encontrará. Existem alguns outros, mas vamos explicá-los quando nos depararmos com eles:
esp
: Registro de 32 bits para armazenar o ponteiro da pilhaebp
: Registro de 32 bits para armazenar o ponteiro baseeax
: Registrador de uso geral de 32 bits, às vezes chamado de "acumulador"ecx
: Registro de uso geral de 32 bitsebx
: Registro de uso geral de 32 bitsedx
: Registro de uso geral de 32 bitsesi
: Registradores de uso geral de 32 bits usados principalmente para carregar e armazenaredi
: Registradores de uso geral de 32 bits usados principalmente para carregar e armazenar
Cada um dos registros de uso geral pode ser referenciado por um valor completo de 32 bits ou por algum subconjunto disso, como os primeiros 8 bits ou segundo 8 bits. Por exemplo, eax
refere-se ao general de 32 bits registradores, mas ax
refere-se aos últimos 16 bits do registrador eax
e al
são os primeiros 8 bits. Dependendo do tipo de dados, o registro está armazenando, podemos fazer referência a diferentes partes.
3.4 O ponteiro base e o ponteiro de pilha
Dois registros serão referenciados mais do que qualquer outro: o de base e o ponteiro de pilha. Esses registros mantêm o estado de referência de memória para a execução atual, com referência ao quadro de função atual. Um quadro de função é parte da memória na pilha que armazena o informações para uma execução de funções atuais, incluindo dados locais e endereços de retorno. O ponteiro base define a parte superior e inferior do quadro de função.
A estrutura de um quadro de função é assim
<- 4 bytes -> .-------------. | ... | higher address ebp+0x8 ->| func args | ebp+0x4 ->| return addr | ebp ->| saved ebp | ebp-0x4 ->| | : : : ' ' ' local args . . . : : : esp+0x4 ->| | esp ->| | lower addreses '-------------'
Movendo-se de endereços mais altos para endereços mais baixos, a parte superior do quadro armazena os argumentos da função. Estes são normalmente referenciados em compensações positivas de registro ebp
. Por exemplo, o primeiro argumento é ebp+0x8
movendo para cima dessa posição.
O segundo item no quadro de função é o endereço de retorno em
ebp+0x4
. O valor armazenado nesta memória é onde a próxima instrução esta após a instrução return, ou qual instrução ocorre após a conclusão da chamada para esta instrução. Vamos gastar MUITO TEMPO falando sobre isso mais tarde.
Finalmente, há o salvo ebp
, este é o endereço onde o último ponteiro base para a função de chamada. Precisamos salvar esse valor para que o quadro de pilha da função de chamada pode ser restaurado uma vez que este função seja concluída.
O ponteiro da pilha faz referência à parte inferior da pilha, o mais baixo endereço alocado. Endereços após este ponto são considerados não alocado. No entanto, é muito fácil alocar mais espaço, vamos apenas subtrair do ponteiro da pilha.
3.5 Gerenciando o quadro de pilha e a pilha
Agora que temos algum contexto para os registros, vamos dar uma olhada no primeiro conjunto de instruções em nosso código:
0x0804841d <+0>: push ebp 0x0804841e <+1>: mov ebp,esp 0x08048420 <+3>: and esp,0xfffffff0 0x08048423 <+6>: sub esp,0x30
Vamos primeiro analisar as quatro primeiras instruções. A instrução push enviará um valor para a pilha e, neste caso, é o anterior ponteiro base, ou seja, o ponteiro base salvo. Em seguida, o ponteiro base é definido como o ponteiro de pilha () (mov
), e, em seguida, alinhado a 4 bits (and
). Em seguida, subtrair do ponteiro da pilha aloca o restante do quadro de pilha, que tem 0x30 bytes ou 48 bytes (não se esqueça sobre o hexadecimal).
3.6 Referenciando, desreferenciando e configurando a memória
O próximo conjunto de instruções autoriza a memória da pilha. Vamos voltar para o código C para ver isso em c antes de olharmos para ele em assembly.
char hello[15]="Hello, World!\n";
A cadeia de caracteres "Hello World!\n" é definida na pilha em um array de caracteres de 15 bytes. Na montagem, isso se parece assim.
0x08048426 <+9>: mov DWORD PTR [esp+0x1d],0x6c6c6548 0x0804842e <+17>: mov DWORD PTR [esp+0x21],0x57202c6f 0x08048436 <+25>: mov DWORD PTR [esp+0x25],0x646c726f 0x0804843e <+33>: mov WORD PTR [esp+0x29],0xa21 0x08048445 <+40>: mov BYTE PTR [esp+0x2b],0x0
Se você apertar os olhos em <src>
para os operadores, reconhecerá que isso é ASCII. Se você não acredita, confira a tabela ASCII. O DWORD ou WORD ou BYTE PTR são comandos de deferência.
BYTE PTR[addr]
: ponteiro de byte: desreferenciar um byte no endereçoWORD PTR[addr]
: ponteiro de palavra: desreferenciar os dois bytes no endereçoDWORD PTR[addr]
: ponteiro de palavra duplo : desreferenciar os quatro bytes no endereço
Outra maneira de ver essas instruções em C seria assim (Não programe assim, no entanto):
char hello[15]; // l l e H * ((int *) (hello)) = 0x6c6c6548; // set hello[0]->hello[3] // W , o * ((int *) (hello + 4)) = 0x57202c6f; // set hello[4]->hello[7] // d l r o * ((int *) (hello + 8)) = 0x646c726f; // set hello[8]->hello[11] // \n ! * ((short *) (hello + 12)) = 0x0a21; // set hello[12]->hello[13] // \0 * ((char *) (hello+14)) = 0x00; // set hello[14]
As próximas duas instruções são um pouco diferentes:
0x0804844a <+45>: lea eax,[esp+0x1d] 0x0804844e <+49>: mov DWORD PTR [esp+0x2c],eax
lea
significa load effective address e é um atalho para fazer um pouco de matemática e calcular um deslocamento do ponteiro e armazená-lo. Se olharmos no que vem a seguir no programa C, vemos que ele está configurando o for-loop.
for(p = hello; *p; p++){
A primeira parte do loop for está inicializando o ponteiro p
para referência ao início da string hello. A partir do código anterior, o início da string hello está no deslocamento de endereço esp+0x1d
e queremos para definir esse endereço com o valor de p
. Este é um processo de duas etapas:
- O endereço real deve ser calculado usando adição de
esp
e armazenado.lea eax,[esp+0x1d]
calculará o endereço e armazenará emeax
. - O valor em
eax
deve ser armazenado na memória reservada parap
, que está no endereçoesp+0x2c
, o comando move faz isso.
Neste ponto, tudo está configurado. E para referência, lembre-se de que o endereço de p
está em esp+0x2c
.
3.7 Loops, Saltos e Testes de condição
Agora, chegamos ao cerne do programa: o loop interno. Nós podemos seguir a execução neste ponto seguindo os saltos.
0x08048452 <+53>: jmp 0x804846b <main+78> # -----------. 0x08048454 <+55>: mov eax,DWORD PTR [esp+0x2c] # <-------. | 0x08048458 <+59>: movzx eax,BYTE PTR [eax] # | | 0x0804845b <+62>: movsx eax,al # | | 0x0804845e <+65>: mov DWORD PTR [esp],eax # | | //loop body 0x08048461 <+68>: call 0x8048310 <putchar@plt> # | | 0x08048466 <+73>: add DWORD PTR [esp+0x2c],0x1 # | | 0x0804846b <+78>: mov eax,DWORD PTR [esp+0x2c] # <-------+--' 0x0804846f <+82>: movzx eax,BYTE PTR [eax] # | //exit condition 0x08048472 <+85>: test al,al # | 0x08048474 <+87>: jne 0x8048454 <main+55> # -------'
A instrução jmp
altera o ponteiro de instrução para o destino especificado. Não é condicionado, é um salto forçado explícito. Seguindo esse salto no código, encontramos as três instruções a seguir:
0x0804846f <+82>: movzx eax,BYTE PTR [eax] 0x08048472 <+85>: test al,al 0x08048474 <+87>: jne 0x8048454 <main+55>
Mais fácil começar com a instrução movzx
. Lembre-se de que neste ponto no código, eax
tem o valor que é o mesmo valor dep
. E você pode ver que é o caso na instrução anterior mov eax,DWORD
PTR [esp+0x2c]
onde esp+0x2c
é o endereço de memória para p.
A instrução movzx
irá deferenciar o endereço armazenado em eax
que é o que referencia p
, ler um byte nesse endereço e escrever nos 8 bits inferiores de eax. Esta é essencialmente *p
a operação que é algum caractere em hello, e então o que queremos testar é se p
referência ao NULL no final de hello.
Esse teste ocorre test al,al
no qual compara ao registrador em diferentes modos. Aqui estamos testando o registrador al
que é o mais baixo dos 8 bits de eax
, aonde nos armazenamos a deferencia de p
. Os resultados de teste, maior que, menor que, igual, não zero, etc. são armazenado em sets de bit flag. Aquele com o qual nos preocupamos é a flag ZF
flag ou a
zero flag. Se al
é zero então ZF
é setado para 1 que seria o caso quando p
referência o final da string hello
.
O comando jne
diz pular quando não for igual a zero.Se for o caso que al
é zero, não pule, caso contrário, continue para o endereço e continue o loop.
3.8 Chamadas de Função
Se investigarmos o corpo do loop, encontraremos as seguintes instruções:
0x08048454 <+55>: mov eax,DWORD PTR [esp+0x2c] 0x08048458 <+59>: movzx eax,BYTE PTR [eax] 0x0804845b <+62>: movsx eax,al 0x0804845e <+65>: mov DWORD PTR [esp],eax 0x08048461 <+68>: call 0x8048310 <putchar@plt>
O primeiro conjunto de instruções, muito parecido com o teste anterior, é deferência do ponteiro p
.
- Carregar o valor
p
, um endereço de memória, emeax
- Leia o byte referenciado em
p
nos 8 bits inferiores deeax
- zerar os bits restantes de
eax
deixar apenas 8 bits inferiores
Neste ponto, eax
armazena um valor como 0x0000048 (i.e, 'H') onde o byte mais baixo é o caractere de interesse e os bytes restantes são 0.
Esse valor é então gravado na parte superior da pilha, conforme referenciado por,
esp
porque estamos prestes a fazer uma chamada de função. Os argumentos para pushed as funções são enviadas para a pilha antes de uma chamada. Neste caso, alocou esse espaço de pilha com antecedência para que não precisemos empurrar, mas o argumento está no lugar certo, no topo da pilha.
A próxima operação é uma call
que executará a função
putchar
, convenientemente nos disse pelo gdb. Uma vez que a função seja concluída, a execução continuará até o ponto logo após o
call
, que é a instrução add
.
0x08048466 <+73>: add DWORD PTR [esp+0x2c],0x1
Olhando atentamente para esta instrução, você vê que isso aumentará o ponteiro p
, e as instruções após o teste de tempo p
agora referência zero. E o loop continua … à medida que o mundo gira.