654 lines
22 KiB
Org Mode
654 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 definindo 'main' no Assembly
|
|||
|
|
|||
|
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 híbrido (Assembly+C)
|
|||
|
|
|||
|
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 em Assembly puro
|
|||
|
|
|||
|
- 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 híbrido (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=
|