653 lines
22 KiB
Org Mode
653 lines
22 KiB
Org Mode
#+title: 6 – Vetor de argumentos de linha de comando
|
||
#+author: Blau Araujo
|
||
#+email: cursos@blauaraujo.com
|
||
|
||
#+options: toc:3
|
||
|
||
* Objetivos
|
||
|
||
- Compreender a passagem de argumentos de linha de comando para processos.
|
||
- Descobrir como o kernel Linux estrutura o vetor de argumentos.
|
||
- Acessar a quantidade e os elementos do vetor de argumentos.
|
||
- Criar programas em Assembly que trabalhem com argumentos de linha de comando.
|
||
- Implementar parsing manual de argumentos no Assembly.
|
||
|
||
* O quadro inicial da pilha do processo
|
||
|
||
Ao iniciar a pilha de um processo, o kernel inclui diversos conjuntos de dados
|
||
relativos ao ambiente de execução do programa e outras informações auxiliares
|
||
para uso do /runtime/ da linguagem C (=glibc=) e do carregados dinâmico do sistema
|
||
(=ld-linux=). Na arquitetura Linux x86_64, dos endereços mais altos para os mais
|
||
baixos, nós temos:
|
||
|
||
- Strings dos argumentos de linha de comando;
|
||
- Strings das definições de variáveis exportadas para o ambiente do processo;
|
||
- Semente aleatória (=AT_RANDOM=);
|
||
- Nome do executável (=AT_EXECFN=);
|
||
- Vetor auxiliar (=auxv=);
|
||
- Vetor dos endereços dos dados de ambiente (=envp=);
|
||
- Vetor dos endereços dos argumentos (=argv=);
|
||
- Contador de argumentos (=argc=).
|
||
|
||
O diagrama abaixo é uma representação simplificada do estado inicial da pilha,
|
||
segundo especificado pela /System V Application Binary Interface/ (ABI) para a
|
||
arquitetura AMD64:
|
||
|
||
#+begin_example
|
||
┌────────────────────────────────┐
|
||
│ NÃO ESPECIFICADO │ ENDEREÇOS MAIS ALTOS
|
||
├────────────────────────────────┤ ─┐
|
||
│ STRINGS DE ARGUMENTOS │ │
|
||
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤ │
|
||
│ STRINGS DE AMBIENTE │ ├─ BLOCO DE INFORMAÇÕES
|
||
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤ │ (SEM ORDEM ESPECÍFICA)
|
||
│ DADOS BINÁRIOS AUXILIARES │ │
|
||
├────────────────────────────────┤ ─┘
|
||
│ NÃO ESPECIFICADO │
|
||
├────────────────────────────────┤
|
||
│ ENTRADA NULA DO VETOR AUXILIAR │ QWORD (0x00)
|
||
├────────────────────────────────┤
|
||
│ ENTRADAS DO VETOR AUXILIAR │ 2 QWORDS POR ENTRADA
|
||
├────────────────────────────────┤
|
||
│ SEPARADOR NULO │ QWORD (0X00)
|
||
├────────────────────────────────┤
|
||
│ VETOR AMBIENTE (ENVP) │ 1 QWORD POR ENDEREÇO
|
||
├────────────────────────────────┤
|
||
RSP + (8 * ARGC) + 8 │ SEPARADOR NULO │ QWORD (0X00)
|
||
├────────────────────────────────┤
|
||
RSP + 8 (ARGV[0]) │ VETOR DE ARGUMENTOS (ARGV) │ 1 QWORD POR ENDEREÇO
|
||
├────────────────────────────────┤
|
||
RSP │ ENDEREÇO DO VALOR DE ARGC │ 1 QWORD
|
||
├────────────────────────────────┤
|
||
│ NÃO DEFINIDO │ ENDEREÇO MAIS BAIXO
|
||
└────────────────────────────────┘
|
||
|
||
QWORD = 8 BYTES (64 BITS)
|
||
#+end_example
|
||
|
||
Embora a ABI diga que o conteúdo do bloco de informações não precisa seguir uma
|
||
ordem específica, o Linux padronizou uma ordem em sua implementação da carga de
|
||
programas (em [[https://github.com/torvalds/linux/blob/94305e83eccb3120c921cd3a015cd74731140bac/fs/binfmt_elf.c#L824][kernel.org]]), dos endereços mais altos para os mais baixos:
|
||
|
||
- Outros eventuais dados auxiliares
|
||
- Strings das definições das variáveis exportadas
|
||
- Strings de argumentos
|
||
- Semente aleatória (=AT_RANDOM=, ponteiro para 16 bytes aleatórios)
|
||
- Endereço da string da arquitetura (=AT_PLATAFORM=)
|
||
- Endereço da string do caminho do executável (=AT_EXECFN=)
|
||
|
||
No contexto deste e do próximo tópico dos nossos estudos, interessam apenas os
|
||
dados do quadro inicial da pilha que, de algum modo, propiciam alguma forma de
|
||
interação do usuário com os nossos programas: os argumentos passados pela linha
|
||
de comandos e as variáveis exportadas para o processo do programa.
|
||
|
||
* Inspecionando argumentos e variáveis exportadas
|
||
|
||
Para investigar o estado do quadro inicial da pilha, nós vamos utilizar o
|
||
programa [[exemplos/06/exit42.asm][exit42.asm]]:
|
||
|
||
#+begin_src asm
|
||
; Retorna 42 como estado de término
|
||
|
||
section .text
|
||
global _start
|
||
|
||
_start:
|
||
mov rax, 60 ; syscall: exit
|
||
mov rdi, 42 ; código de saída
|
||
syscall
|
||
#+end_src
|
||
|
||
*Montagem e link-edição:*
|
||
|
||
#+begin_example
|
||
:~$ nasm -f elf64 -g exit42.asm
|
||
:~$ ld -o exit42 exit42.o
|
||
#+end_example
|
||
|
||
Agora, vamos abrir o executável no GDB, definir =_start= como ponto de parada
|
||
e iniciar sua execução:
|
||
|
||
#+begin_example
|
||
:~$ gdb exit42
|
||
Reading symbols from exit42...
|
||
(gdb) break _start
|
||
Breakpoint 1 at 0x401000: file exit42.asm, line 7.
|
||
(gdb) run
|
||
Starting program: /home/blau/git/pbn/curso/exemplos/06/exit42
|
||
|
||
Breakpoint 1, _start () at exit42.asm:7
|
||
7 mov rax, 60 ; syscall: exit
|
||
(gdb)
|
||
#+end_example
|
||
|
||
** Contador de argumentos
|
||
|
||
Primeiro, vamos examinar o conteúdo do registrador =rsp= (/stack pointer/):
|
||
|
||
#+begin_example
|
||
(gdb) info registers rsp
|
||
rsp 0x7fffffffdfd0 0x7fffffffdfd0
|
||
#+end_example
|
||
|
||
Como resultado, nós temos um endereço que, segundo o diagrama anterior, deve
|
||
nos levar à quantidade de argumentos de linha de comando escritos para invocar
|
||
o programa (=argc=). Como o programa não foi iniciado com argumentos, o valor de
|
||
=argc= deve ser =1= (escrito em 8 bytes), correspondendo ao caminho do nosso
|
||
executável. Sendo assim, vamos utilizar o comando =x= para exibir uma /giant-word/
|
||
em hexa (=/1gx=) a partir do endereço em =rsp= (=$rsp=):
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp
|
||
0x7fffffffdfd0: 0x0000000000000001
|
||
#+end_example
|
||
|
||
E lá está o valor =1= escrito em 8 bytes.
|
||
|
||
#+begin_quote
|
||
*Nota:* É por conta dessa ambiguidade do termo /argumentos/, utilizado tanto no
|
||
sentido de /"argumentos passados para um programa"/ quanto de /"todas as palavras
|
||
utilizadas para invocar o programa"/, que o vetor de argumentos também é
|
||
chamado de /vetor de parâmetros/, bem mais adequado para expressar o que nós
|
||
encontramos no quadro inicial da pilha (todas as palavras utilizadas para
|
||
invocar o programa). Mas, feita a observação, vamos nos manter na terminologia
|
||
da documentação da ABI e do kernel.
|
||
#+end_quote
|
||
|
||
** Primeiro elemento do vetor de argumentos
|
||
|
||
Lembrando que o registrador =rsp= sempre contém o dado que está no topo da
|
||
pilha (ou seja, em seu endereço mais baixo) e sabendo que o dado anterior
|
||
está 8 bytes acima, nós podemos obter o endereço do primeiro elemento do
|
||
vetor de argumentos (=argv=) desta forma:
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp+8
|
||
0x7fffffffdfd8: 0x00007fffffffe2ec
|
||
#+end_example
|
||
|
||
Se o endereço estiver correto, ele nos levará à string do caminho completo
|
||
do nosso executável. Para confirmar, vamos examinar com o formato =/1s=, de
|
||
/"uma string"/:
|
||
|
||
#+begin_example
|
||
(gdb) x /1s 0x00007fffffffe2ec
|
||
0x7fffffffe2ec: "/home/blau/git/pbn/curso/exemplos/06/exit42"
|
||
#+end_example
|
||
|
||
Confirmado!
|
||
|
||
** Separador nulo do vetor de argumentos
|
||
|
||
Ainda segundo o diagrama do quadro inicial da pilha, tendo apenas um argumento,
|
||
a posição seguinte na pilha (=rsp+16=) deve conter um endereço que nos leva a
|
||
uma cadeia de 8 bytes zerados (/separador nulo/). Então, vejamos o que mostra
|
||
o GDB:
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp+16
|
||
0x7fffffffdfe0: 0x0000000000000000
|
||
#+end_example
|
||
|
||
Não é o que estamos vendo... Na verdade, o que esperávamos resulta de uma
|
||
interpretação equivocada bastante comum das especificações da ABI. Os dados
|
||
no bloco de informações são compactados, ou seja, escritos sem separação de
|
||
forma contígua. Sendo assim, a delimitação dos vetores (os /separadores nulos/)
|
||
é feita pelos valores dos elementos mais acima na pilha (nos endereços mais
|
||
baixos), como vimos no GDB. A posição do separador nulo de =argv= pode ser
|
||
calculado com:
|
||
|
||
#+begin_example
|
||
rsp + (argc * 8) + 8
|
||
#+end_example
|
||
|
||
Como o valor em =argc= é =1=:
|
||
|
||
#+begin_example
|
||
rsp + (1 * 8) + 8 = rsp + 8 + 8 = rsp + 16
|
||
#+end_example
|
||
|
||
Ou seja, nós teríamos que encontrar o valor =0= escrito com 8 bytes na posição
|
||
da pilha resultante de =rsp + 16= -- e encontramos.
|
||
|
||
** Primeiro elemento do vetor ambiente
|
||
|
||
A posição imediatamente abaixo da pilha (=rsp + 24=) deve conter o endereço da
|
||
primeira string com a definição de uma variável exportada para o processo do
|
||
programa -- ou seja, a string em =envp[0]=. Então, vamos examinar novamente:
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp+24
|
||
0x7fffffffdfe8: 0x00007fffffffe318
|
||
(gdb) x /1s 0x00007fffffffe318
|
||
0x7fffffffe318: "SHELL=/bin/bash"
|
||
#+end_example
|
||
|
||
O quadro inicial da pilha não tem uma informação sobre a quantidade de
|
||
variáveis exportadas para o ambiente do processo, então é difícil saber
|
||
onde ela termina sem continuar a percorrer a pilha até encontrar o separador
|
||
nulo de =envp=. No meu sistema, eu testei manualmente e descobri que o endereço
|
||
do último elemento de =envp= está em =rsp + 368=:
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp+368
|
||
0x7fffffffe140: 0x00007fffffffefa3
|
||
(gdb) x /1s 0x00007fffffffefa3
|
||
0x7fffffffefa3: "OLDPWD=/home/blau/git/pbn/curso/exemplos"
|
||
#+end_example
|
||
|
||
Portanto, em =rsp + 376= eu devo encontrar o separador nulo:
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp+376
|
||
0x7fffffffe148: 0x0000000000000000
|
||
#+end_example
|
||
|
||
E lá está ele!
|
||
|
||
** Resultados com mais de um argumento
|
||
|
||
Também é interessante investigar o que acontece com o quadro inicial da
|
||
pilha quando nós executamos o programa com outros argumentos de linha de
|
||
comando além, é claro, de seu próprio caminho. Para isso, vamos iniciar
|
||
novamente o GDB, mas vamos rodar o programa com argumentos:
|
||
|
||
#+begin_example
|
||
$ gdb exit42
|
||
Reading symbols from exit42...
|
||
(gdb) b _start
|
||
Breakpoint 1 at 0x401000: file exit42.asm, line 7.
|
||
(gdb) run banana laranja
|
||
Starting program: /home/blau/git/pbn/curso/exemplos/06/exit42 banana laranja
|
||
|
||
Breakpoint 1, _start () at exit42.asm:7
|
||
7 mov rax, 60 ; syscall: exit
|
||
#+end_example
|
||
|
||
*Conferindo =argc=:*
|
||
|
||
#+begin_example
|
||
(gdb) info registers rsp
|
||
rsp 0x7fffffffdfb0 0x7fffffffdfb0
|
||
(gdb) x /1gx 0x7fffffffdfb0
|
||
0x7fffffffdfb0: 0x0000000000000003
|
||
#+end_example
|
||
|
||
*Valor em =argv[0]=:*
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp + 8
|
||
0x7fffffffdfb8: 0x00007fffffffe2dd
|
||
(gdb) x /1s 0x00007fffffffe2dd
|
||
0x7fffffffe2dd: "/home/blau/git/pbn/curso/exemplos/06/exit42"
|
||
#+end_example
|
||
|
||
*Valor em =argv[1]=:*
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp + 16
|
||
0x7fffffffdfc0: 0x00007fffffffe309
|
||
(gdb) x /1s 0x00007fffffffe309
|
||
0x7fffffffe309: "banana"
|
||
#+end_example
|
||
|
||
*Valor em =argv[2]=:*
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp + 24
|
||
0x7fffffffdfc8: 0x00007fffffffe310
|
||
(gdb) x /1s 0x00007fffffffe310
|
||
0x7fffffffe310: "laranja"
|
||
#+end_example
|
||
|
||
*Separador nulo de =argv=:*
|
||
|
||
#+begin_example
|
||
(gdb) x /1gx $rsp + 32
|
||
0x7fffffffdfd0: 0x0000000000000000
|
||
#+end_example
|
||
|
||
* Acesso aos argumentos em baixo nível
|
||
|
||
Em Assembly, as duas técnicas mais comuns para acessar os argumentos de linha
|
||
de comando são a leitura da pilha após o ponto de entrada (=_start=) e, no caso
|
||
de programas híbridos que seguem as convenções da linguagem C, exportando uma
|
||
função =main=, deixando que a =glibc= cuide da pilha e das convenções de chamada.
|
||
|
||
** Acesso direto à pilha
|
||
|
||
Basta salvar o endereço em =rsp= e utilizá-lo para obter =argc= e os demais
|
||
elementos em =argv=:
|
||
|
||
#+begin_src asm
|
||
section .bss
|
||
argc: resq 1 ; reservar 8 bytes para receber o valor de argc.
|
||
argv: resq 1 ; reservar 8 bytes para receber o endereço de argv[0].
|
||
|
||
section .text
|
||
global _start
|
||
|
||
_start:
|
||
; ----------------------------------------------------
|
||
; Inicialização de argc e argv...
|
||
; ----------------------------------------------------
|
||
mov rbx, rsp ; endereço de argc
|
||
mov rax, [rbx] ; valor de argc
|
||
lea rcx, [rbx + 8] ; endereço de argv[0]
|
||
mov [argc], rax
|
||
mov [argv], rcx
|
||
|
||
; Agora podemos usar [argc] e [argv] em qualquer parte do código.
|
||
; ...
|
||
#+end_src
|
||
|
||
#+begin_quote
|
||
*Nota:* a instrução =lea= (/Load Effective Address/) calcula um endereço e carrega
|
||
o resultado em um registrador de destino.
|
||
#+end_quote
|
||
|
||
** Acesso em Assembly exportando main
|
||
|
||
Exportando a rotina principal do programa com o nome =main=, nós podemos gerar
|
||
o executável com o =gcc= que, utilizando o objeto de inicialização da linguagem
|
||
C (=crt0.o=), executará todas as tarefas de preparação para o acesso aos dados
|
||
de argumentos e ambiente, o que inclui:
|
||
|
||
- Carregar =argc= em =rdi=;
|
||
- Carregar o endereço de =argv[0]= em =rsi=;
|
||
- Carregar o endereço de =envp[0]= em =rdx=;
|
||
- Configurar variáveis globais para a =glibc= (se usada);
|
||
- Chamar =main=, esperando que retorne um =int= em =eax=;
|
||
- Terminar o programa chamando a função =exit= (=glibc=).
|
||
|
||
O exemplo abaixo imprime o nome do programa (string =argv[0]=) utilizando a
|
||
função =printf=, da glibc:
|
||
|
||
#+begin_src asm :tangle exemplos/06/prog.asm
|
||
section .data
|
||
fmt db "Programa: %s", 10, 0
|
||
|
||
global main ; símbolo chamado por _start de crt0.o
|
||
extern printf ; (opcional) usar funções da libc
|
||
|
||
section .text
|
||
main:
|
||
; argc → rdi
|
||
; argv → rsi
|
||
; envp → rdx (não obrigatório)
|
||
|
||
; Exemplo: imprimir argv[0]
|
||
mov rdi, fmt
|
||
mov rsi, [rsi] ; argv[0]
|
||
xor rax, rax ; terminação de argumentos (printf é variádica)
|
||
call printf
|
||
|
||
mov eax, 0 ; retorno de 'main' = (int)0
|
||
ret
|
||
#+end_src
|
||
|
||
Para montar e compilar:
|
||
|
||
#+begin_example
|
||
:~$ nasm -g -f elf64 prog.asm
|
||
:~$ gcc -no-pie -o prog prog.o
|
||
#+end_example
|
||
|
||
Atenção para a opção =-no-pie= no =gcc=: Isso é necessário porque o exemplo não
|
||
foi escrito nem montado prevendo suporte a endereçamento relativo (PIE).
|
||
|
||
Executando:
|
||
|
||
#+begin_example
|
||
:~$ ./prog
|
||
Programa: ./prog
|
||
#+end_example
|
||
|
||
Mas note que, ao preparar a chamada de =printf=, nós tivemos que seguir as
|
||
convenções da ABI e passar o primeiro argumento em =rdi= e o segundo em =rsi=:
|
||
que são justamente os registradores onde as informações de argumentos foram
|
||
carregadas. Portanto, se quisermos reutilizar esses dados, nós devemos
|
||
salvá-los em outro lugar, como fizemos antes:
|
||
|
||
#+begin_src asm
|
||
section .bss
|
||
argc: resq 1 ; reservar 8 bytes para receber o valor de argc.
|
||
argv: resq 1 ; reservar 8 bytes para receber o endereço de argv[0].
|
||
|
||
section .data
|
||
fmt db "Programa: %s", 10, 0
|
||
|
||
global main ; símbolo chamado por _start de crt0.o
|
||
extern printf ; (opcional) usar funções da libc
|
||
|
||
section .text
|
||
main:
|
||
; argc → rdi
|
||
; argv → rsi
|
||
; envp → rdx (não obrigatório)
|
||
mov [argc], rdi ; salvar argc
|
||
mov [argv], rsi ; salvar endereço de argv[0]
|
||
|
||
; Exemplo: imprimir argv[0]
|
||
mov rdi, fmt
|
||
mov rsi, [argv] ; argv[0]
|
||
xor rax, rax ; terminação de argumentos (printf é variádica)
|
||
call printf
|
||
|
||
mov eax, 0 ; retorno de 'main' = (int)0
|
||
ret
|
||
#+end_src
|
||
|
||
* Listando todos os dados de argumentos
|
||
|
||
Para encerrar este tópico, vamos criar dois programas que acessam dos dados de
|
||
argumentos na pilha e lista seus valores no terminal: um utilizando o /runtime/
|
||
da linguagem C (=crt0.o=) e outro totalmente em Assembly, mas com um pequeno
|
||
módulo de sub-rotinas para conversão e impressão que só será estudada nos
|
||
próximos tópicos.
|
||
|
||
** Exemplo em Assembly com main e gcc
|
||
|
||
Arquivo: [[exemplos/06/cargs.asm][cargs.asm]]
|
||
|
||
#+begin_src asm :tangle exemplos/06/cargs.asm
|
||
section .bss
|
||
argc: resq 1 ; 8 bytes para armazenar argc
|
||
argv: resq 1 ; 8 bytes para armazenar ponteiro para argv[0]
|
||
|
||
section .data
|
||
fmt_argc db "argc : %ld", 10, 0
|
||
fmt_argv db "argv[%d]: %s", 10, 0
|
||
|
||
global main
|
||
extern printf
|
||
|
||
section .text
|
||
main:
|
||
; rdi = argc
|
||
; rsi = argv
|
||
mov [argc], rdi ; salvar argc
|
||
mov [argv], rsi ; salvar argv[0]
|
||
|
||
; Imprimir com: printf("argc : %ld", argc);
|
||
mov rdi, fmt_argc ; primeiro argumento: formato
|
||
mov rsi, [argc] ; segundo argumento: argc (qword = long)
|
||
xor rax, rax ; terminação de argumentos
|
||
call printf
|
||
|
||
; preparar o laço: i = 0
|
||
xor rcx, rcx ; i = 0
|
||
mov rbx, [argv] ; rbx = argv base
|
||
mov r8, [argc] ; r8 = argc
|
||
|
||
.loop:
|
||
cmp rcx, r8 ; compara i com argc
|
||
jge .done ; se i >= argc, fim
|
||
|
||
; Imprimir com: printf("argv[%d]: %s\n", i, argv[i]);
|
||
mov rdi, fmt_argv ; primeiro argumento: formato
|
||
mov rsi, rcx ; segundo argumento: i
|
||
mov rdx, [rbx + rcx*8] ; terceiro argumento: argv[i]
|
||
xor rax, rax ; terminação de argumentos
|
||
push rcx ; salvar rcx (caller saved) na pilha
|
||
push r8 ; salvar r8 (caller saved) na pilha
|
||
call printf
|
||
pop r8 ; restaurar rcx
|
||
pop rcx ; restaurar rcx
|
||
|
||
inc rcx
|
||
jmp .loop
|
||
|
||
.done:
|
||
xor eax, eax ; retorno de main: (int)0
|
||
ret
|
||
#+end_src
|
||
|
||
Compilando e executando:
|
||
|
||
#+begin_example
|
||
:~$ nasm -g -felf64 cargs.asm
|
||
:~$ gcc -no-pie -o cargs cargs.o
|
||
:~$ ./cargs a b c
|
||
argc : 4
|
||
argv[0]: ./cargs
|
||
argv[1]: a
|
||
argv[2]: b
|
||
argv[3]: c
|
||
#+end_example
|
||
|
||
*** Nota sobre registradores /"caller saved"/
|
||
|
||
#+begin_src asm
|
||
push rcx ; salvar rcx (caller saved) na pilha
|
||
push r8 ; salvar r8 (caller saved) na pilha
|
||
call printf
|
||
pop r8 ; restaurar rcx
|
||
pop rcx ; restaurar rcx
|
||
#+end_src
|
||
|
||
Nesse trecho, nós salvamos na pilha os registradores =rcx= e =r8=, usados como
|
||
referências do nosso /loop/, antes de chamar a função =printf=. Isso foi
|
||
necessário porque, segundo a ABI, eles estão entre os registradores /"caller
|
||
saved"/, ou seja: registradores que podem ser alterados pela chamada de uma
|
||
função com a instrução =call= e, portanto, terão que ser salvos pela função
|
||
chamadora, caso estejam em uso para outros propósitos.
|
||
|
||
São registradores /caller-saved/ segundo a ABI (System V AMD64):
|
||
|
||
- =rax=: usado para retorno de funções;
|
||
- =rcx=: usado como argumento 4 em chamadas de funções;
|
||
- =rdx=: usado como argumento 3 em chamadas de funções;
|
||
- =rsi=: usado como argumento 2 em chamadas de funções;
|
||
- =rdi=: usado como argumento 1 em chamadas de funções;
|
||
- =r8= e =r9=: usados como argumentos de 5 e 6 em chamadas de funções;
|
||
- =r10= e =r11=: uso interno em funções.
|
||
|
||
A volatilidade desses registradores não está relacionada com seu uso antes
|
||
da chamada da função (=call=), mas com o fato da ABI não obrigar a função
|
||
chamada a salvar e restaurar seus conteúdos.
|
||
|
||
** Exemplo com acesso direto à pilha
|
||
|
||
- Arquivo: [[exemplos/06/args.asm][args.asm]]
|
||
- Módulo: [[exemplos/06/print_utils.asm][print_utils.asm]]
|
||
|
||
Neste exemplo, não há necessidade de salvar =argc= e =argv= na memória.
|
||
|
||
#+begin_src asm
|
||
; ----------------------------------------------------------
|
||
; Montar módulo com : nasm -f elf64 print_utils.asm
|
||
; Montar programa com : nasm -f elf64 args.asm
|
||
; Gerar executável com: ls -o args args.o print_utils.o
|
||
; ----------------------------------------------------------
|
||
section .rodata
|
||
pref_argc db "argc : ", 0 ; Prefixo da impressão de argc
|
||
pref_argv db "argv[", 0 ; Prefixo da impressão de argv
|
||
posf_argv db "]: ", 0 ; Sufixo da impressão de argv
|
||
|
||
; sub-rotinas do módulo print_utils.o
|
||
extern print_int
|
||
extern print_str
|
||
extern print_char
|
||
|
||
section .text
|
||
global _start
|
||
|
||
_start:
|
||
; ----------------------------------------------------------
|
||
; Impressão de argc
|
||
; ----------------------------------------------------------
|
||
mov rdi, pref_argc ; endereço de pref_argc
|
||
call print_str ; imprime pref_argc
|
||
|
||
mov rax, [rsp] ; rax = argc
|
||
call print_int ; imprime argc
|
||
|
||
mov rdi, 10 ; rdi = \n
|
||
call print_char ; imprime \n
|
||
; ----------------------------------------------------------
|
||
; Impressão de argv
|
||
; ----------------------------------------------------------
|
||
mov rcx, 0 ; índice i = 0
|
||
lea rbx, [rsp + 8] ; rbx = endereço de argv[0]
|
||
|
||
.print_argv_loop:
|
||
cmp rcx, [rsp] ; enquanto i < argc
|
||
jge .done_argv
|
||
|
||
mov rdi, pref_argv ; imprime prefixo "argv["
|
||
call print_str
|
||
|
||
mov rax, rcx ; rax = i
|
||
call print_int ; imprime índice
|
||
|
||
mov rdi, posf_argv ; imprime sufixo "]: "
|
||
call print_str
|
||
|
||
mov rdi, [rbx + rcx*8] ; rdi = argv[i]
|
||
call print_str
|
||
|
||
mov rdi, 10 ; imprime \n
|
||
call print_char
|
||
|
||
inc rcx
|
||
jmp .print_argv_loop
|
||
|
||
.done_argv:
|
||
; ----------------------------------------------------------
|
||
; Termina o programa com exit(0)
|
||
; ----------------------------------------------------------
|
||
mov rax, 60 ; syscall: exit
|
||
xor rdi, rdi ; status = 0
|
||
syscall
|
||
#+end_src
|
||
|
||
Montagem, link-edição e execução:
|
||
|
||
#+begin_example
|
||
:~$ nasm -f elf64 print_utils.asm
|
||
:~$ nasm -f elf64 args.asm
|
||
:~$ ld -o args args.o print_utils.o
|
||
:~$ ./args a b c d
|
||
argc : 5
|
||
argv[0]: ./args
|
||
argv[1]: a
|
||
argv[2]: b
|
||
argv[3]: c
|
||
argv[4]: d
|
||
#+end_example
|
||
|
||
* Exercícios propostos
|
||
|
||
1. Crie um programa em Assembly que receba seu nome como argumento e imprima =Salve, NOME!=.
|
||
2. Crie uma versão desse mesmo programa em Assembly chamando funções da linguagem C.
|
||
3. Utilizando o GDB e o exemplo =cargs.asm=, analise o comportamento dos registradores voláteis nas chamadas de funções da =glibc=.
|
||
4. Com base nos exemplos do tópico, crie um programa que liste as variáveis no vetor de ambiente.
|
||
|
||
* Referências
|
||
|
||
- [[https://refspecs.linuxfoundation.org/][System V Application Binary Interface – AMD64 Architecture Processor Supplement]]
|
||
- [[https://github.com/torvalds/linux/blob/master/fs/binfmt_elf.c][Linux kernel: fs/binfmt_elf.c]]
|
||
- =man 2 execve=
|
||
- =man 3 environ=
|
||
- =man 7 exec=
|