A Pilha no Início do Programa: Argumentos e Variáveis de Ambiente em Assembly NASM para Linux
Quando o Linux carrega um programa na memória, ele prepara uma área de memória chamada pilha (stack). No momento em que a primeira instrução do programa é executada (no nosso caso, no rótulo _start), essa pilha não está vazia. Ela já contém informações cruciais passadas pelo sistema operacional.
A estrutura da pilha, do endereço mais alto para o mais baixo, é a seguinte:
+----------------------------------------------------+ | Ponteiro Nulo (terminador do ambiente) | +----------------------------------------------------+ | Ponteiro para a n-ésima variável de ambiente | +----------------------------------------------------+ | ... | +----------------------------------------------------+ | Ponteiro para a primeira variável de ambiente | +----------------------------------------------------+ | Ponteiro Nulo (terminador dos argumentos) | +----------------------------------------------------+ | Ponteiro para o n-ésimo argumento da linha de comando | +----------------------------------------------------+ | ... | +----------------------------------------------------+ | Ponteiro para o primeiro argumento (argv[1]) | +----------------------------------------------------+ | Ponteiro para o nome do programa (argv[0]) | +----------------------------------------------------+ | Número de argumentos da linha de comando (argc) | <--- RSP aponta aqui +----------------------------------------------------+
No ponto de entrada _start, o ponteiro da pilha RSP aponta diretamente para o número de argumentos da linha de comando (argc). Este valor inclui o nome do próprio programa, então ele será sempre no mínimo 1. Logo acima na pilha, encontramos uma sequência de ponteiros para as strings dos argumentos (argv), terminada por um ponteiro nulo. Acima disso, há uma sequência semelhante de ponteiros para as variáveis de ambiente.
Obtendo o Número de Argumentos
Podemos facilmente obter argc lendo o valor no topo da pilha.
global _start
section .text
_start:
mov rdi, [rsp] ; Obtém argc em RDI (será o código de saída)
mov rax, 60
syscallVamos compilar e testar com diferentes números de argumentos:
$ nasm -felf64 hello.asm -o hello.o $ ld hello.o -o hello $ ./hello $ echo $? 1
Sem argumentos, o código de saída é 1 (apenas o nome do programa).
$ ./hello arg1 arg2 arg3 $ echo $? 4
Com três argumentos, o código de saída é 4 (3 argumentos + o nome do programa).
Imprimindo o Nome do Programa
Para imprimir uma string, precisamos de seu endereço e de seu tamanho. O endereço do nome do programa (argv[0]) está em [RSP+8]. Para encontrar o tamanho, precisamos localizar o byte nulo (\0) que a termina. A instrução repne scasb é perfeita para isso.
global _start
section .text
_start:
; 1. Encontra o tamanho da string
mov rdi, [rsp+8] ; RDI = endereço do nome do programa (argv[0])
xor rax, rax ; AL = 0 (o byte que estamos procurando)
mov rcx, -1 ; Um número muito grande para o contador
repne scasb ; Procura pelo byte nulo
; Agora, RDI aponta para o byte *após* o nulo.
; A diferença entre o ponteiro final e o inicial é o tamanho.
mov rdx, rdi ; Salva o ponteiro final
sub rdx, [rsp+8] ; RDX = tamanho da string (incluindo o byte nulo)
dec rdx ; RDX = tamanho da string (excluindo o byte nulo)
; 2. Imprime a string
mov rdi, 1 ; 1º parâmetro: stdout
mov rsi, [rsp+8] ; 2º parâmetro: endereço da string
; RDX já contém o tamanho
mov rax, 1 ; syscall write
syscall
mov rax, 60
syscallAo executar, o programa imprimirá como foi chamado:
$ ./hello ./hello
Lendo e Imprimindo Todos os Argumentos
Podemos iterar sobre a lista de ponteiros de argumentos para imprimir todos eles.
global _start
section .text
_start:
mov rbx, 0 ; RBX = índice do argumento atual
mov r15, [rsp] ; R15 = argc
scan_args:
; Pega o endereço do argumento atual
mov rdi, [rsp + 8 + rbx*8]
; Calcula o tamanho da string
push rbx ; Salva os registradores que serão modificados
push rdi
xor rax, rax
mov rcx, -1
repne scasb
pop rsi ; Recupera o endereço inicial em RSI
pop rbx
sub rdi, rsi ; RDI = tamanho
dec rdi
; Imprime o argumento
mov rdx, rdi
mov rdi, 1
; rsi já contém o endereço do argumento
mov rax, 1
syscall
; Imprime uma nova linha
mov rax, 1
mov rdi, 1
lea rsi, [newline]
mov rdx, 1
syscall
inc rbx ; Próximo argumento
cmp rbx, r15 ; Já processamos todos?
jne scan_args ; Se não, repete o laço
mov rax, 60
syscall
section .data
newline: db 10O programa percorre todos os ponteiros de argv[0] até argv[argc-1], calcula o tamanho de cada string e a imprime no console, seguida por uma quebra de linha.
Resultado da execução:
$ ./hello arg1 arg2 arg3 ./hello arg1 arg2 arg3
A mesma técnica pode ser aplicada para ler as variáveis de ambiente, que estão localizadas na pilha logo após a lista de argumentos.
Resumo
- No ponto de entrada
_startde um programa Linux, a pilha já contém o número de argumentos (argc), um array de ponteiros para os argumentos (argv), e um array de ponteiros para as variáveis de ambiente. RSPaponta paraargc.[RSP+8]aponta para o nome do programa (argv[0]).- Os argumentos da linha de comando podem ser acessados iterando sobre os ponteiros em
[RSP + 8 + índice*8]. - A instrução
repne scasbé uma forma eficiente de calcular o comprimento de strings terminadas com byte nulo.