#+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=
