From 31905503d311657067a8befc461bca52980066e0 Mon Sep 17 00:00:00 2001 From: Blau Araujo Date: Sun, 25 May 2025 12:28:10 -0300 Subject: [PATCH] =?UTF-8?q?conte=C3=BAdo=20da=20aula=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- curso/aula-06.org | 653 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 653 insertions(+) create mode 100644 curso/aula-06.org diff --git a/curso/aula-06.org b/curso/aula-06.org new file mode 100644 index 0000000..daecd89 --- /dev/null +++ b/curso/aula-06.org @@ -0,0 +1,653 @@ +#+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=