03 - Arquitetura x86-64
No handout passado usamos o gdb para listar variáveis globais, nomes de funções e examinar endereços de memória. Neste handout vamos começar a usar o gdb também para examinar nossos programas durante sua execução.
Parando e continuando a execução de um programa.
Compile funcoes.c usando as flags:
Tip 1
Na compilação, -Og
irá permitir otimizações de código que não interfiram no debugging. Já o parâmetro -g
irá tornar disponíveis informações úteis para o debugging (como tipos de variáveis e cabeçalhos das funções)
Exercise 1
Exercise 2
Answer
(gdb) disas funcao1
Dump of assembler code for function funcao1:
0x000000000000117f <+0>: endbr64
0x0000000000001183 <+4>: mov %edi,%eax
0x0000000000001185 <+6>: add (%rsi),%eax
0x0000000000001187 <+8>: ret
End of assembler dump.
Os registradores %edi
, %eax
armazenam valores de 4 bytes e o registrado %rsi
armazena um endereço de 8 bytes.
Exercise 3
Answer
O registrador %edi
armazena o valor da variável int a
e o registrador %rsi
representa o argumento int *b
, e por fim, o retorno da função, variável int c
, é armazenado na variável %eax
.
Exercise 4
Answer
A execução do programa para exatamente na entrada da função int funcao1(int a, int *b)
.
(gdb) run
Starting program: /home/luba/00-Aulas/03-arquitetura-x86/funcoes
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, funcao1 (a=a@entry=10, b=b@entry=0x7fffffffdc80) at funcoes.c:10
10 int funcao1(int a, int *b) {
Após executar o run
, utilize disas funcao1
e observe no código da função uma seta indicando o próximo comando a ser executado, que representa o breakpoint no início da função.
Dica 2
Os comandos info breakpoints
, info b
ou ainda i b
podem ser utilizados para listar os breakpoints inseridos no código!
Exercise 5
Exercise 6
Answer
A instrução em que o programa está parado é apontada pela seta =>
Exercise 7
Answer
A instrução mov %edi,%eax
move o valor de %edi
para %eax
, em seguida, a instrução add (%rsi),%eax
adiciona o conteúdo apontado pelo registrador %rsi
ao registrador %eax
, pois %rsi
é um ponteiro.
Exercise 8
Exercise 9
Exercise 10
Tip 3
Execute o comando continue
para continuar rodando o programa. Ele irá rodar até que o próximo breakpoint seja alcançado ou até que o programa termine.
Endereçamento relativo e variáveis globais
Na parte anterior analisamos o código Assembly de nossa primeira função e vimos como
- mostrar o código fonte de uma função usando
disas
- mostrar o conteúdo de um registrador usando
info registers
- executar exatamente uma instrução usando
stepi
Também vimos que ao colocar um registrador entre ( )
estamos fazendo um acesso a memória. Esta operação é equivalente a desreferenciar um ponteiro usando *p
. Neste roteiro iremos adicionar um detalhe importante: podemos fazer contas com endereços usando esta notação. Nos exemplo abaixo nos referimos a memória como um grande vetor de bytes unsigned char M[]
. Ou seja, ao acessar M[%rax]
, por exemplo, estamos acessando o lugar na memória cujo endereço está escrito em %rax
.
10(%rax)
: acessa a memóriaM[%rax + 10]
.(%rax, %rdi, 4)
: acessa a memóriaM[%rax + 4 * % rdi]
. Note que isto se parece com aritmética de ponteiros cujo tipo apontado seja inteiro, pois os endereços pulam de 4 em 4 bytes.
Exercise 11
Exercise 12
Exercise 13
Answer
A instrução mov 0x2ea1(%rip),%eax
faz leitura do valor da variável global times_called
representado pelo endereço 0x2ea1(%rip)
, a instrução mov %eax,0x2e98(%rip)
faz a escrita do valor de %eax
na variável global times_called
.
Exercise 14
Answer
Registrador %rip
(Instruction Pointer Register), às vezes também chamado de Program Counter, guarda o endereço da próxima instrução a ser executada pela CPU.
O tipo de acesso a memória que estamos realizando se chama rip relative addressing
. Este tipo de acesso é reservado para variáveis globais e dados somente leitura. Estes dados tem uma característica especial: eles são copiados para a memória seguindo o mesmo layout do arquivo executável. Ou seja, as posições relativas entre o código e os dados globais são fixas.
Exercise 15
Desenho de acesso a memória usando %rip
A figura abaixo ilustra como funciona o endereçamento usando o registrador %rip
. Note que os deslocamentos são diferentes pois o endereço da instrução atual é diferente. Porém, o resultado final do endereço calculado em ambas instruções é o mesmo, indicando que ambas se referem ao mesmo local na memória.
Exercise 16
Além de poder mostrar valores na memória podemos escrever valores também. A sintaxe usada é a seguinte:
set *( (tipo *) 0x.....) = valor
onde devemos substituir tipo
por um tipo básico de C, 0x...
pelo endereço desejado e valor
pelo valor que queremos escrever. Note que o que estamos fazendo é um cast do endereço 0x....
para um ponteiro de tipo
e depois estamos acessando o valor apontado usando *
!
Exercise 18
Desafio
Localize na função main
as chamadas ao comando printf
, analise as chamadas para responder o próximo exercício!
Exercise 19
Atividade para entrega!
Aula com atividade para entrega. Confira seu repositório de entregas do classroom!
Atenção
Faça git pull
no seu repositório de entregas que irá aparecer uma nova pasta dentro de atv
.
Dica 4
Leia o README.md
disponível na pasta da atividade para descobrir como resolver e entregar.