artigo sobre linguagens de alto e baixo nível

This commit is contained in:
Blau Araujo 2025-05-16 19:10:10 -03:00
parent 025a49ef3b
commit 3efbda0330

View file

@ -0,0 +1,764 @@
#+title: 2 -- Linguagens de Montagem e a Compilação
#+author: Blau Araujo
#+email: cursos@blauaraujo.com
#+options: toc:3
* Objetivos
- Compreender o papel das linguagens de montagem.
- Distinguir montagem, compilação e linkagem.
- Conhecer o funcionamento do NASM e do `ld`.
- Utilizar ferramentas como `objdump` e `readelf`.
* Do código-fonte ao binário
Quando programamos um computador, geralmente escrevemos textos que correspondem
a instruções em linguagens que nós, humanos, conseguimos ler com maior ou menor
facilidade. Os arquivos com os textos escritos nessas linguagens são chamados
de /códigos-fonte/, mas o processador da máquina (a sua CPU) não é capaz de
interpretá-los diretamente. Para a CPU, instruções são os conjuntos de sinais
elétricos digitais que ela identifica como /códigos de operação/ (/OpCodes/) ou,
de forma mais genérica, como /código de máquina/.
** Arquivos texto e binários
O caminho entre o que nós escrevemos nos códigos-fonte dos nossos programas e o
código de máquina, que a CPU consegue executar, envolve toda uma série de
transformações que resulta na montagem daquilo que nós chamamos de /arquivo
binário executável/. No fundo, todo arquivo é binário, ou seja, todos eles contêm
informações que podem ser convertidas em estados elétricos alto ou baixo que,
no fim das contas, equivalem a números escritos na base 2 (números /binários/).
#+begin_quote
*Curiosidade:* consegue notar que programas são apenas números gigantescos?
#+end_quote
O que diferencia os arquivos texto, em relação aos binários, é que os números
que eles contêm são escritos de modo a codificar a representação de caracteres
utilizados para formar textos (codificação ASCII, por exemplo). Nos arquivos
binários, por sua vez, o que importa é a codificação de dados, que podem
representar diversos tipos de informações, como os /pixels/ de uma imagem, a
intensidade sonora ao longo do tempo de uma música ou, no nosso caso, as
instruções de um programa em código de máquina.
** Formato de binário executável
Voltando aos arquivos binários executáveis, além das instruções e dados do
programa, eles devem conter informações estruturadas que serão utilizadas pelo
sistema operacional para, primeiro, determinar se o arquivo é um binário
executável válido e, segundo, para definir como suas várias partes serão
copiadas para a memória, se ele puder ser executado. A estrutura do binário,
portanto, deve obedecer às convenções específicas de cada sistema operacional
para arquivos executáveis. Esse conjunto de regras é chamado de /formato de
executável/ e no sistema GNU/Linux (e sistemas /Unix-like/, em geral) é utilizado
o formato ELF (de /Executable and Linkable Format/), que é aplicado a:
- Arquivos binários executáveis
- Arquivos objeto
- Bibliotecas compartilhadas
- Imagens de memória geradas em casos de falha (/core dumps/)
A parte /"linkable"/ do formato ELF refere-se ao fato de que os arquivos objeto
gerados podem ser ligados (isto é, combinados com outros objetos binários) para
formar programas executáveis completos, seja por meio de ligações estáticas ou
dinâmicas:
- *Ligação estática:* todos os objetos e bibliotecas são combinados em um único
binário executável.
- *Ligação dinâmica:* o binário executável contém referências a bibliotecas
externas que serão carregadas na memória em tempo de execução.
Seja qual for o modo de ligação, os objetos e bibliotecas compartilhadas devem
seguir as especificações do formato ELF.
* Programação em código de máquina
É possível, embora raro e extremamente trabalhoso, escrever diretamente em
código de máquina: ou seja, especificando manualmente os bytes que representam
as instruções da CPU. Abaixo, nós temos o arquivo texto com os 96 bytes de um
programa escrito diretamente em código de máquina ([[exemplos/02/ok.txt][ok.txt]]):
#+begin_example
7f 45 4c 46 01 01 01 00 00 00 00 00
00 00 00 00 02 00 03 00 01 00 00 00
54 80 04 08 34 00 00 00 00 00 00 00
00 00 00 00 34 00 20 00 01 00 28 00
00 00 00 00 01 00 00 00 54 00 00 00
54 80 04 08 00 00 00 00 0c 00 00 00
0c 00 00 00 05 00 00 00 00 10 00 00
b8 01 00 00 00 bb 00 00 00 00 cd 80
#+end_example
Para criar um arquivo binário com esses bytes, eu posso utilizar o utilitário
=xxd=, geralmente disponível quando se instala o editor Vim:
#+begin_example
:~$ xxd -r -p ok.txt > ok.bin
:~$ ls
ok.bin ok.txt
#+end_example
Onde:
- =-r=: Impressão reversa (de texto para binário).
- =-p=: Despejo bruto hexadecimal (apenas os dígitos válidos para hexadecimais).
- =>=: Operador de redirecionamento de escrita do shell.
O =xxd= também pode ser utilizado para visualizar o conteúdo do arquivo binário:
#+begin_example
:~$ xxd ok.bin
00000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............
00000010: 0200 0300 0100 0000 5480 0408 3400 0000 ........T...4...
00000020: 0000 0000 0000 0000 3400 2000 0100 2800 ........4. ...(.
00000030: 0000 0000 0100 0000 5400 0000 5480 0408 ........T...T...
00000040: 0000 0000 0c00 0000 0c00 0000 0500 0000 ................
00000050: 0010 0000 b801 0000 00bb 0000 0000 cd80 ................
#+end_example
Para facilitar as próximas demonstrações, nós vamos formatar a impressão em
12 colunas com grupos de apenas 1 byte:
#+begin_example
:~$ xxd -c 12 -g 1 ok.bin
00000000: 7f 45 4c 46 01 01 01 00 00 00 00 00 .ELF........
0000000c: 00 00 00 00 02 00 03 00 01 00 00 00 ............
00000018: 54 80 04 08 34 00 00 00 00 00 00 00 T...4.......
00000024: 00 00 00 00 34 00 20 00 01 00 28 00 ....4. ...(.
00000030: 00 00 00 00 01 00 00 00 54 00 00 00 ........T...
0000003c: 54 80 04 08 00 00 00 00 0c 00 00 00 T...........
00000048: 0c 00 00 00 05 00 00 00 00 10 00 00 ............
00000054: b8 01 00 00 00 bb 00 00 00 00 cd 80 ............
#+end_example
Os primeiros 4 bytes (=7f 45 4c 46=) são o /número mágico/ que identifica o formato
ELF para o sistema operacional. Na coluna da representação em ASCII, na saída
do =xxd=, nós podemos ver que os 3 últimos bytes do número mágico correspondem
aos caracteres =ELF=. Isso pode ser encontrado em qualquer arquivo binário, por
exemplo, em um arquivo de imagem PNG:
#+begin_example
:~$ xxd -c 12 -g 1 foto.png | head -1
00000000: 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d .PNG........
#+end_example
A primeira coluna da saída do =xxd= mostra, em hexadecimal a posição que o
primeiro byte de cada linha impressa ocupa no arquivo. Com base nisso, observe
que tudo que vem antes do byte =0x54= (byte na posição 84) são dados calculados
ou convencionados para atender às especificações do formato de arquivos
binários executáveis ELF32 (32 bits) -- o programa em si, são apenas os 12
últimos bytes:
#+begin_example
00000054: b8 01 00 00 00 bb 00 00 00 00 cd 80 ............
#+end_example
Aqui, estão os códigos de operação (/OpCodes/) da arquitetura x86 e seus
respectivos operandos. Em Assembly 32 bits, eles corresponderiam a:
#+begin_example
b8 01 00 00 00 -> mov eax, 0x00000001 ; Registrar o valor 1 em EAX (syscall exit).
bb 00 00 00 00 -> mov ebx, 0x00000000 ; Registrar o valor 0 em EBX (estado de término).
cd 80 -> int 0x80 ; Executar a interrupção 0x80 (executa a syscall).
#+end_example
Nós falaremos sobre registradores e chamadas de sistemas mais adiante, mas
esta tradução "reversa" (de código de máquina para Assembly) mostra que,
seguindo as convenções de chamadas de sistema do Linux, eu copiei o valor
=1= no registrador =EAX= para informar que queria executar a chamada de sistema
=exit=, copiei =0= em =EBX= para definir o valor retornado pelo programa ao
terminar (estado de término) e mandei executar a interrupção =0x80= que,
em arquiteturas 32 bits, é como as chamadas de sistema são executadas.
Resumindo, meu programa não faz nada além de terminar com estado =0= (que
significa /sucesso/, para o sistema). Para confirmar, vamos dar permissão
de execução ao arquivo binário e observar seu estado de término:
#+begin_example
:~$ chmod +x ok.bin
:~$ ./ok.bin
:~$ echo $?
0
#+end_example
Mas podemos alterar o programa de modo que ele terminasse retornando,
por exemplo, o valor =42=. Para isso, nós precisamos de um editor de arquivos
binários, como o =hexedit= ou o =bvi=. Como sou mais familiarizado com os
editores Vi e Vim, eu vou utilizar o =bvi=. Para iniciar a edição:
#+begin_example
:~$ bvi ok.bin
#+end_example
No editor, nós veremos isso:
#+begin_example
00000000 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00 02 00 03 00 .ELF................
00000014 01 00 00 00 54 80 04 08 34 00 00 00 00 00 00 00 00 00 00 00 ....T...4...........
00000028 34 00 20 00 01 00 28 00 00 00 00 00 01 00 00 00 54 00 00 00 4. ...(.........T...
0000003C 54 80 04 08 00 00 00 00 0C 00 00 00 0C 00 00 00 05 00 00 00 T...................
00000050 00 10 00 00 B8 01 00 00 00 BB 00 00 00 00 CD 80 ................
#+end_example
O valor que eu quero alterar está na linha iniciada pelo byte na posição
=0x50=, logo depois do /OpCode/ =BB= (=MOV EBX=). O valor atual é formado por
quatro bytes (32 bits) escritos na ordem /little endian/, ou seja, os bytes
menos significativos vêm antes dos mais significativos. Portanto, se nós
queremos escrever =0x0000002a= (valor hexadecimal de 42 em 4 bytes), o
último byte (=2a=) deverá vir na frente dos demais.
Para iniciar a edição do arquivo, nós precisamos executar o comando:
#+begin_example
:set memmove
#+end_example
Agora, posicionando o cursor no byte que queremos substituir, nós teclamos
=s= e digitamos =2a=, o que deixará a linha editada assim:
#+begin_example
00000050 00 10 00 00 B8 01 00 00 00 BB 2A 00 00 00 CD 80 ..........*.....
#+end_example
Teclando =ESC=, para voltar ao modo normal, nós podemos salvar e sair
com o comando:
#+begin_example
:wq
#+end_example
Verificando o novo conteúdo do arquivo binário:
#+begin_example
:~$ xxd -c 12 -g 1 ok.bin
00000000: 7f 45 4c 46 01 01 01 00 00 00 00 00 .ELF........
0000000c: 00 00 00 00 02 00 03 00 01 00 00 00 ............
00000018: 54 80 04 08 34 00 00 00 00 00 00 00 T...4.......
00000024: 00 00 00 00 34 00 20 00 01 00 28 00 ....4. ...(.
00000030: 00 00 00 00 01 00 00 00 54 00 00 00 ........T...
0000003c: 54 80 04 08 00 00 00 00 0c 00 00 00 T...........
00000048: 0c 00 00 00 05 00 00 00 00 10 00 00 ............
00000054: b8 01 00 00 00 bb 2a 00 00 00 cd 80 ......*.....
#+end_example
Como o arquivo já tem as permissões necessárias, nós podemos executá-lo
e verificar seu estado de término:
#+begin_example
:~$ ./ok.bin
:~$ echo $?
42
#+end_example
* Linguagens de baixo e alto nível
Criar e editar binários em código de máquina já foi comum nos primórdios da
computação e ainda aparece em contextos de engenharia reversa, fins didáticos
ou em casos altamente especializados, mas é impraticável para a criação de
programas de maior complexidade. Por isso, foram desenvolvidas linguagens
que, embora ainda muito próximas da escrita de código de máquina, tornaram
esse processo mais viável.
É o caso da linguagem Assembly, classificada como uma linguagem de /baixo
nível/ porque, embora abstraia a escrita do código de máquina por meio de
mnemônicos e pseudo instruções legíveis por humanos, ainda nos obriga a
descrever, passo a passo, cada uma das instruções que a CPU deverá executar
para alcançar a finalidade do programa. Já nas linguagens de alto nível, os
passos da CPU são abstraídos, de modo que nós podemos descrever nossos
programas em termos de procedimentos que devem ser realizados, em vez de
como esses procedimentos são implementados para serem executados pela
máquina.
#+begin_quote
*Nota:* É por isso que se diz que, com linguagens de alto nível, nos escrevemos
/"o que fazer"/, ao passo que, com linguagens de baixo nível nós escrevemos
/"como fazer"/.
#+end_quote
Independentemente de estar escrito em uma linguagem de baixo ou alto nível,
todo programa precisa, em algum momento, ser convertido em código de máquina
para que possa ser executado pela CPU. Isso pode ocorrer por meio de tradução
direta -- como nos processos de compilação e montagem -- ou de forma indireta,
através da interpretação por outro programa que já esteja traduzido para código
de máquina. Em outras palavras, os códigos-fonte só poderão ser executados se
forem compilados, montados ou interpretados, dependendo das características da
linguagem utilizada.
** Linguagens interpretadas
No caso das linguagens interpretadas, como Lua, Python ou PHP, por exemplo,
é o binário do interpretador que é executado para ler, analisar e executar
o conteúdo do código-fonte, conforme as suas instruções são processadas. Isso
significa que o código de máquina é produzido em tempo real: o que é bastante
flexível e prático, mas pode apresentar um desempenho inferior, se comparado
com programas executados diretamente a partir de seus binários.
De modo geral, programas interpretados passam pelas seguintes etapas até
serem executados:
- Leitura ::
O programa interpretador lê uma linha ou um bloco do código-fonte.
- Análise léxica ::
O trecho lido é dividido em palavras-chave, identificadores, operadores e
outros símbolos significativos (um processo chamado de /tokenização/).
- Análise sintática ::
Os elementos léxicos são organizados conforme as regras gramaticais da
linguagem, formando estruturas como expressões, comandos e blocos de
controle.
- Execução ::
O interpretador, com base na estrutura sintática encontrada, executa a
operação correspondente por meio de rotinas internas que já estão em
código de máquina.
O processo se repete até que todo o conteúdo do código-fonte seja interpretado
e suas instruções executadas.
** Linguagens compiladas
O termo /compilação/ é utilizado para designar, de forma geral, o processo de
traduzir um código escrito numa linguagem de alto nível (como C, C++, Rust
ou Pascal) para um arquivo binário no formato que o sistema computacional
em questão seja capaz de carregar na memória e executar. Essa tradução
é feita por programas chamados de /compiladores/, mas eles podem atuar de
formas diferentes, a depender do conceito que pretendam implementar.
Tomando a linguagem C como exemplo, compiladores como o =cc= (comum em sistemas
BSD e Unix) e o =gcc= (do Projeto GNU) realizam o processo de compilação em até
quatro etapas principais:
- Pré-processamento ::
As diretivas encontradas no código-fonte (como =#include= e =#define=), além de
outros requisitos implícitos, são processadas para gerar uma versão expandida
do programa, ainda escrita em C.
Podemos interromper a compilação após esta etapa e inspecionar o resultado com:
#+begin_example
gcc -E arquivo.c
#+end_example
O resultado é impresso no terminal, sem gerar nenhum arquivo.
- Compilação ::
O código C pré-processado é traduzido para linguagem Assembly. Esse passo pode
ocorrer de forma interna, mas também pode ser observado explicitamente gerando
um arquivo com extensão =.s=:
#+begin_example
gcc -S arquivo.c
#+end_example
Note que esta etapa também é chamada de /compilação/, porque o termo se aplica
tanto ao processo completo quanto a essa etapa específica.
- Montagem ::
O código em Assembly (interno ou gerado) é convertido em um arquivo objeto
binário, normalmente com extensão =.o=. Podemos interromper a compilação aqui
com:
#+begin_example
gcc -c arquivo.c
#+end_example
- Link-edição (ou apenas /ligação/) ::
O arquivo objeto é combinado com outros arquivos objeto (caso haja) e com
as bibliotecas necessárias. Isso pode resultar na junção de código proveniente
de bibliotecas estáticas (ligações estáticas) e na inserção de referências
a bibliotecas compartilhadas (ligações dinâmicas).
Ao final, nós teremos um binário executável formatado de acordo com o padrão
do sistema (como ELF, no Linux). Essa etapa é realizada pelo programa =ld=,
geralmente invocado automaticamente pelo compilador. Contudo, mesmo no caso
da linguagem C, existem compiladores que adotam propostas diferentes, gerando
código de máquina sem passar explicitamente por uma etapa intermediária de
tradução para Assembly.
** Linguagens de montagem
Quando se trata de linguagens de baixo nível, o que nós temos é, basicamente,
um conjunto de mnemônicos para os /OpCodes/ da CPU e algumas pseudoinstruções
para definir estruturas de dados e organizar as seções do que virá a ser o
arquivo binário do programa. Em outras palavras, um código em Assembly pode
ser visto como um "manual de construção" do código de máquina: daí o nome
/"assembly"/, que significa /"montagem"/ em inglês. Consequentemente, o programa
utilizado para montar o arquivo binário com o código de máquina a partir de
um fonte em linguagem de montagem é chamado de "montador" -- ou /"assembler"/,
em inglês.
As pequenas (embora importantes) diferenças entre as várias linguagens de
montagem existentes são determinadas, essencialmente, pelas particularidades
dos montadores que as implementam e pelas especificações das arquiteturas de
hardware e software para as quais esses montadores foram projetados. Além
disso, montadores como o NASM (/Netwide Assembler/) e GAS (/GNU Assembler/), por
exemplo, não produzem arquivos binários executáveis diretamente. Isso tem
a vantagem de possibilitar o uso dos arquivos objeto gerados de várias formas,
mas requer que uma etapa posterior de link-edição seja realizada para que se
tenha um binário executável. Já o montador FASM (/Fast Assembler/), também
bastante utilizado, dispensa a etapa final de link-edição, mas sua integração
com código em C não é tão direta ou automatizada quanto em montadores como
o NASM, exigindo mais controle manual por parte de quem programa.
Neste curso, a escolha do montador NASM deve-se a alguns motivos:
- Utiliza a sintaxe Intel por padrão.
- Tem um suporte a macros simples e poderoso.
- Tem um ótimo suporte à integração com código em C.
- É uma linguagem fartamente documentada.
- Pode ser aprendida com muita facilidade por iniciantes.
* Exemplos comparativos
** Programa em Assembly
Aqui, nós temos um programa escrito em Assembly 64 bits (NASM) para imprimir
a mensagem =Salve, simpatia!= no terminal e sair com estado de término =0=...
Arquivo: [[exemplos/02/salve.asm][salve.asm]]
#+begin_src nasm :tangle exemplos/02/salve.asm
section .rodata
msg db "Salve, simpatia!", 10
len equ $ - msg
section .text
global _start
_start:
mov rax, 1 ; syscall write
mov rdi, 1 ; stdout
mov rsi, msg
mov rdx, len
syscall
mov rax, 60 ; syscall exit
mov rdi, 0 ; código de saída 0
syscall
#+end_src
Por enquanto, não importa entender o que está no código, mas observar seu
aspecto geral que, claramente, define passo a passo o que a CPU deve fazer
e organiza os dados e as instruções nas seções que deverão ocupar no
arquivo binário resultante. A mensagem que queremos imprimir, por exemplo,
está na seção chamada de =.rodata=, que é onde dados constantes são escritos
no arquivo binário.
Montando e link-editando o programa:
#+begin_example
:~$ nasm -f elf64 -o salve.o salve.asm
:~$ ld -o salve salve.o
#+end_example
Como não foram exibidas mensagens de erro ou de aviso, nós sabemos que
tudo correu bem e já podemos executar o programa:
#+begin_example
:~$ ./salve
Salve, simpatia!
#+end_example
** Inspeção do arquivo binário
*** Tamanho do binário em bytes
#+begin_example
:~$ stat -c '%s' salve
8880
#+end_example
*** Informações gerais do arquivo
#+begin_example
:~$ file salve
salve: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked,
not stripped
#+end_example
*** Cabeçalho ELF
#+begin_example
:~$ readelf -h salve
Cabeçalho ELF:
Magia: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Classe: ELF64
Dados: complemento 2, little endian
Versão: 1 (actual)
OS/ABI: UNIX - System V
Versão ABI: 0
Tipo: EXEC (ficheiro executável)
Máquina: Advanced Micro Devices X86-64
Versão: 0x1
Endereço do ponto de entrada: 0x401000
Início dos cabeçalhos do programa: 64 (bytes no ficheiro)
Start of section headers: 8496 (bytes no ficheiro)
Bandeiras: 0x0
Tamanho deste cabeçalho: 64 (bytes)
Tamanho dos cabeçalhos do programa:56 (bytes)
Nº de cabeçalhos do programa: 3
Tamanho dos cabeçalhos de secção: 64 (bytes)
Nº dos cabeçalhos de secção: 6
Índice de tabela de cadeias da secção: 5
#+end_example
*** Lista de seções
#+begin_example
:~$ readelf -l salve
Tipo de ficheiro Elf é EXEC (ficheiro executável)
Entry point 0x401000
There are 3 program headers, starting at offset 64
Cabeçalhos do programa:
Tipo Desvio EndVirtl EndFís
TamFich TamMem Bndrs Alinh
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000e8 0x00000000000000e8 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000025 0x0000000000000025 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x0000000000000011 0x0000000000000011 R 0x1000
Secção para mapa do segmento:
Secções do segmento...
00
01 .text
02 .rodata
#+end_example
*** Conteúdo (em hexa) da seção .text
#+begin_example
:~$ readelf -x .text salve
Despejo máximo da secção ".text":
0x00401000 b8010000 00bf0100 000048be 00204000 ..........H.. @.
0x00401010 00000000 ba110000 000f05b8 3c000000 ............<...
0x00401020 4831ff0f 05 H1...
#+end_example
#+begin_src sh
file salve
readelf -h salve
objdump -d salve
#+end_src
*** Conteúdo (em hexa) da seção .rodata
#+begin_example
:~$ readelf -x .rodata salve
Despejo máximo da secção ".rodata":
0x00402000 53616c76 652c2073 696d7061 74696121 Salve, simpatia!
0x00402010 0a .
#+end_example
** Versão equivalente em C
Tentando ser o mais próximo possível ao que foi escrito em Assembly, esta
seria uma possível versão equivalente em C...
Arquivo: [[exemplos/02/salve.c][salve.c]]
#+begin_src C :tangle exemplos/02/salve.c
#include <unistd.h>
// msg db "Salve, simpatia!", 10
const char msg[] = "Salve, simpatia!\n";
// len equ $ - msg
#define LEN sizeof(msg) - 1
int main(void) {
/*
mov rax, 1 ; syscall write
mov rdi, 1 ; stdout
mov rsi, msg
mov rdx, len
syscall
,*/
write(1, msg, LEN);
/*
mov rax, 60 ; syscall exit
mov rdi, 0 ; código de saída 0
syscall
,*/
_exit(0);
}
#+end_src
Compilando e executando:
#+begin_example
:~$ gcc -o salvec salve.c
:~$ ./salvec
Salve, simpatia!
#+end_example
** Inspeção do binário produzido com o código em C
*** Tamanho do binário em bytes:
#+begin_example
:~$ stat -c '%s' salvec
16032
#+end_example
*** Informações gerais do arquivo
#+begin_example
:~$ file salvec
salvec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=351e5395c71e0b758b9cc05a68f3813f8f130817, for GNU/Linux 3.2.0,
not stripped
#+end_example
*** Cabeçalho ELF
#+begin_example
:~$ readelf -h salvec
Cabeçalho ELF:
Magia: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Classe: ELF64
Dados: complemento 2, little endian
Versão: 1 (actual)
OS/ABI: UNIX - System V
Versão ABI: 0
Tipo: DYN (Position-Independent Executable file)
Máquina: Advanced Micro Devices X86-64
Versão: 0x1
Endereço do ponto de entrada: 0x1060
Início dos cabeçalhos do programa: 64 (bytes no ficheiro)
Start of section headers: 14048 (bytes no ficheiro)
Bandeiras: 0x0
Tamanho deste cabeçalho: 64 (bytes)
Tamanho dos cabeçalhos do programa:56 (bytes)
Nº de cabeçalhos do programa: 14
Tamanho dos cabeçalhos de secção: 64 (bytes)
Nº dos cabeçalhos de secção: 31
Índice de tabela de cadeias da secção: 30
#+end_example
*** Lista de seções
#+begin_example
:~$ readelf -l salvec
Tipo de ficheiro Elf é DYN (Position-Independent Executable file)
Entry point 0x1060
There are 14 program headers, starting at offset 64
Cabeçalhos do programa:
Tipo Desvio EndVirtl EndFís
TamFich TamMem Bndrs Alinh
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x0000000000000310 0x0000000000000310 R 0x8
INTERP 0x0000000000000394 0x0000000000000394 0x0000000000000394
0x000000000000001c 0x000000000000001c R 0x1
[A pedir interpretador do programa: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000660 0x0000000000000660 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000179 0x0000000000000179 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000118 0x0000000000000118 R 0x1000
LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000250 0x0000000000000258 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0
0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000350 0x0000000000000350 0x0000000000000350
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000370 0x0000000000000370 0x0000000000000370
0x0000000000000024 0x0000000000000024 R 0x4
NOTE 0x00000000000020f8 0x00000000000020f8 0x00000000000020f8
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000350 0x0000000000000350 0x0000000000000350
0x0000000000000020 0x0000000000000020 R 0x8
GNU_EH_FRAME 0x0000000000002024 0x0000000000002024 0x0000000000002024
0x000000000000002c 0x000000000000002c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000230 0x0000000000000230 R 0x1
Secção para mapa do segmento:
Secções do segmento...
00
01 .interp
02 .note.gnu.property .note.gnu.build-id .interp .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .text .fini
04 .rodata .eh_frame_hdr .eh_frame .note.ABI-tag
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id
09 .note.ABI-tag
10 .note.gnu.property
11 .eh_frame_hdr
12
13 .init_array .fini_array .dynamic .got
#+end_example
*** Conteúdo (hexa) da seção .text
#+begin_example
:~$ readelf -x .text salvec
Despejo máximo da secção ".text":
0x00001060 31ed4989 d15e4889 e24883e4 f0505445 1.I..^H..H...PTE
0x00001070 31c031c9 488d3dce 000000ff 153f2f00 1.1.H.=......?/.
0x00001080 00f4662e 0f1f8400 00000000 0f1f4000 ..f...........@.
0x00001090 488d3d89 2f000048 8d05822f 00004839 H.=./..H.../..H9
0x000010a0 f8741548 8b051e2f 00004885 c07409ff .t.H.../..H..t..
0x000010b0 e00f1f80 00000000 c30f1f80 00000000 ................
0x000010c0 488d3d59 2f000048 8d35522f 00004829 H.=Y/..H.5R/..H)
0x000010d0 fe4889f0 48c1ee3f 48c1f803 4801c648 .H..H..?H...H..H
0x000010e0 d1fe7414 488b05ed 2e000048 85c07408 ..t.H......H..t.
0x000010f0 ffe0660f 1f440000 c30f1f80 00000000 ..f..D..........
0x00001100 f30f1efa 803d152f 00000075 2b554883 .....=./...u+UH.
0x00001110 3dca2e00 00004889 e5740c48 8b3df62e =.....H..t.H.=..
0x00001120 0000e829 ffffffe8 64ffffff c605ed2e ...)....d.......
0x00001130 0000015d c30f1f00 c30f1f80 00000000 ...]............
0x00001140 f30f1efa e977ffff ff554889 e5ba1100 .....w...UH.....
0x00001150 0000488d 05b70e00 004889c6 bf010000 ..H......H......
0x00001160 00e8dafe ffffbf00 000000e8 c0feffff ................
#+end_example
*** Conteúdo (hexa) da seção .rodata
#+begin_example
:~$ readelf -x .rodata salvec
Despejo máximo da secção ".rodata":
0x00002000 01000200 00000000 00000000 00000000 ................
0x00002010 53616c76 652c2073 696d7061 74696121 Salve, simpatia!
0x00002020 0a00 ..
#+end_example
* Referências
- man xxd
- man 2 write
- man 2 exit
- man readelf
- man bvi
- man stat
- man file
- https://nasm.us/doc/