Finalmente o Hello World.
Geralmente o "Hello World" é a primeira coisa que vemos quando estamos aprendendo uma linguagem de programação. Nesse caso eu deixei por último pois acredito que seria de extrema importância entender todos os conceitos antes de vê-lo, isso evitaria a intuição de ver um código em Assembly como um "código em C mais difícil de ler". Acredito que essa comparação mental involuntária é muito ruim e prejudicaria o aprendizado. Por isso optei por explicar tudo antes mesmo de apresentar o famoso "Hello World".
Desta vez vamos escrever um código em Assembly sem misturar com C, será um executável do Linux (formato ELF64) fazendo chamadas de sistema diretamente. Vamos vê-lo logo:
Para compilar esse código basta usar o NASM especificando o format elf64 e desta vez iremos usar o linker do pacote GCC diretamente. O nome do executável é ld e o uso básico é bem simples, basta especificar o nome do arquivo de saída com -o. Ficando assim:
Na linha 5 definimos uma constante usando o símbolo $ para pegar o endereço da instrução atual e subtraímos pelo endereço do rótulo msg
. Isso resulta no tamanho do texto porque msg
aponta para o início da string e, como está logo em seguida, $ seria o endereço do final da string.
final - início = tamanho
Como deve ter reparado usamos mais uma syscall, que foi a syscall write
. Essa syscall basicamente escreve dados em um arquivo. O primeiro argumento é um número que serve para identificar o arquivo para o qual queremos escrever os dados.
No Linux a saída e entrada de um programa nada mais é que dados sendo escritos e lidos em arquivos. E isso é feito por três arquivos que estão por padrão abertos em um programa e tem sempre o mesmo file descriptor, são eles:
Se quiser ver o código de implementação desta syscall no Linux, pode ver aqui.
Reparou que nosso programa tem um símbolo _start
e que magicamente esse é o código que o sistema operacional está executando primeiro? Isso acontece porque o linker definiu o endereço daquele símbolo como o entry point (ponto de entrada) do nosso programa.
O entry point nada mais é o que o próprio nome sugere, o endereço inicial de execução do programa. Eu sei o que você está pensando:
Então a função main de um programa em C é o entry point?
A resposta é não! Um programa em C usando a libc tem uma série de códigos que são executados antes da main. E o primeiro deles, pasme, é uma função chamada _start
definida pela própria libc.
Na verdade qualquer símbolo pode ser definido como o entry point para o executável, não faz diferença qual nome você dá para ele. Só que _start
é o símbolo padrão que o ld define como entry point.
Se você quiser usar um símbolo diferente é só especificar com a opção -e. Por exemplo, podemos reescrever nosso Hello World assim:
E compilar assim:
Fácil fazer um "Hello World", né? Ei, o que acha de fazer uns macros para melhorar o uso dessas syscalls aí? Seria interessante também salvar os macros em um arquivo separado e incluir o arquivo com a diretiva %include
.
Nome
RAX
RDI
RSI
RDX
write
1
file_descriptor
endereço
tamanho (em bytes)
Nome
File descriptor
Descrição
stdin
0
Entrada de dados (o que é digitado pelo usuário)
stdout
1
Saída padrão (o que é impresso no terminal)
stderr
2
Saída de erro (também impresso no terminal, porém destinado a mensagens de erro)