diff --git a/curso/aula-06.org b/curso/aula-06.org index daecd89..143bfad 100644 --- a/curso/aula-06.org +++ b/curso/aula-06.org @@ -346,7 +346,7 @@ _start: o resultado em um registrador de destino. #+end_quote -** Acesso definindo 'main' no Assembly +** 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 @@ -446,7 +446,7 @@ 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) +** Exemplo em Assembly com main e gcc Arquivo: [[exemplos/06/cargs.asm][cargs.asm]] @@ -547,7 +547,7 @@ 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 +** Exemplo com acesso direto à pilha - Arquivo: [[exemplos/06/args.asm][args.asm]] - Módulo: [[exemplos/06/print_utils.asm][print_utils.asm]] @@ -640,7 +640,7 @@ argv[4]: d * 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). +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. diff --git a/curso/aula-07.org b/curso/aula-07.org new file mode 100644 index 0000000..c00a802 --- /dev/null +++ b/curso/aula-07.org @@ -0,0 +1,456 @@ +#+title: Aula 7 – Vetor de ambiente (envp) +#+author: Blau Araujo +#+email: cursos@blauaraujo.com + +#+options: toc:3 + +* Objetivos + +- Entender o papel do vetor de ambiente (=envp=). +- Acessar as definições de variáveis exportadas para o processo. +- Exemplificar o uso do conteúdo de =envp= em programas escritos em baixo nível. + +* Revisão: o quadro inicial da pilha do processo + +Como vimos no [[aula-06.org][último tópico]], a pilha do processo é inicializada com diversos +conjuntos de dados relativos ao ambiente de execução do programa e outras +informações auxiliares para uso da /runtime/ da linguagem C. Também vimos que o +conteúdo inicial da pilha pode ser representado desta forma: + +#+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 + +* Localizando o vetor de ambiente + +O endereço inicial do vetor de ambiente (=envp=) é carregado na pilha 8 bytes +depois do endereço do separador nulo do vetor de argumentos. Sendo assim, nós +podemos determinar sua posição com: + +#+begin_example +rsp + (8 * argc) + 16 +#+end_example + +Para confirmar, vamos utilizar novamente o programa =exit42.asm= e o GDB: + +Arquivo: [[exemplos/07/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, início do GDB e execução do programa: + +#+begin_example +:~$ nasm -g -f elf64 exit42.asm +:~$ ld -o exit42 exit42.o +:~$ gdb ./exit42 +Reading symbols from ./exit42... +(gdb) run +Starting program: /home/blau/git/pbn/curso/exemplos/07/exit42 + +Breakpoint 1, _start () at exit42.asm:7 +7 mov rax, 60 ; syscall: exit +#+end_example + +O separador nulo, entre =argv= e =envp= deve estar em =rsp+16=, e nós podemos +verificar se é verdade com: + +#+begin_example +(gdb) x /1gx $rsp+16 +0x7fffffffdfe0: 0x0000000000000000 +#+end_example + +Como o programa foi executado sem argumentos, =argc= terá valor =1=. Portanto, +o endereço de =envp[0]= estará 8 bytes depois em =rsp+24=: + +#+begin_example +(gdb) x /1gx $rsp+24 +0x7fffffffdfe8: 0x00007fffffffe318 +#+end_example + +Nós poderíamos exibir a string neste endereço copiando e colando a saída do +comando anterior, mas também é possível exibi-la com a sintaxe: + +#+begin_example +x /1s *((char **)($rsp + 24)) +#+end_example + +Onde: + +- =x=: comando /execute/. +- =/1s=: exibir conteúdo formatado como uma string (cadeia de caracteres terminada com =0x00=). +- =*(...ENDEREÇO...)=: exibir os dados em =ENDEREÇO=. +- =(char **)ENDEREÇO=: definição do tipo do dado em =ENDEREÇO= (ponteiro de ponteiro para caracteres). +- =($rsp + 24)=: o dado que está 24 bytes após o endereço em =rsp=. + +Executando o comando: + +#+begin_example +(gdb) x /1s *((char **)($rsp + 24)) +0x7fffffffe318: "SHELL=/bin/bash" +#+end_example + +E aqui nós temos a string da definição da primeira variável exportada no +processo do nosso programa -- uma string no formato ~NOME=VALOR~. Infelizmente, +diferente do vetor de argumentos, o vetor de ambiente não tem uma informação +de contagem associada (como =argc=), então não temos como saber onde as variáveis +terminam, a não ser percorrendo todos os endereços em =envp= até encontrarmos +outro separador nulo. + +Na prática, porém, isso não é um problema, pois as exportação de variáveis +para processos tem usos muito específicos e são mais utilizadas por programas +escritos em linguagens que, de algum modo, abstraem a busca pelos nomes das +variáveis e a extração de seus respectivos valores. Este é o caso, por exemplo, +da função =getenv=, da =glibc=, declarada em =stdlib.h= como: + +#+begin_src c +char *getenv(const char *name); +#+end_src + +Recebendo o ponteiro para a string de um nome como argumento (=name=), a função +retorna o endereço do byte da string onde o valor atribuído à variável +exportada inicia: + +#+begin_example + endereço de 'name' + ↓ + NOME=VALOR + ↑ + endereço retornado +#+end_example + +* Extraindo valores de variáveis exportadas + +O programa abaixo acessa as variáveis exportadas para o ambiente do processo +(=envp=) e simula o funcionamento de =getenv=, imprimindo o valor de uma variável +buscada, se ela existir. Com este exemplo, nossos objetivos são demonstrar o +uso de argumentos de linha de comando e o acesso ao vetor de ambiente em um +programa escrito sem qualquer abstração, totalmente em baixo nível. + +#+begin_quote +Os detalhes sobre as chamadas de sistema envolvidas e criação de sub-rotinas +ficarão para os próximos tópicos. +#+end_quote + +Arquivo: [[exemplos/07/getenv.asm][getenv.asm]] + +#+begin_src asm :tangle exemplos/07/getenv.asm +; ---------------------------------------------------------- +; Montagem: nasm -f elf64 -o getenv.o getenv_v2.asm +; Ligação : ld -o getenv getenv.o +; Uso : ./getenv NOME_VARIAVEL +; ---------------------------------------------------------- +section .rodata +; ---------------------------------------------------------- + newline db 10, 0 ; caractere de nova linha + msg_nf db "Not found", 10, 0 ; mensagem de erro + msg_usage db "Usage: ./getenv VAR", 10, 0 ; mensagem de uso incorreto +; ---------------------------------------------------------- +section .bss +; ---------------------------------------------------------- + argc resq 1 ; quantidade de argumentos (argc) + argv resq 1 ; endereço do vetor de argumentos (argv) + envp resq 1 ; endereço do vetor de ambiente (envp) +; ---------------------------------------------------------- +section .text +; ---------------------------------------------------------- +global _start +_start: +; ---------------------------------------------------------- +; Carregar e salvar argc, argv e envp +; ---------------------------------------------------------- + mov rax, [rsp] ; argc está no topo da pilha + mov [argc], rax ; salva argc em argc + + lea rax, [rsp + 8] ; argv começa logo após argc + mov [argv], rax ; salva ponteiro de argv + + mov rbx, [argc] + lea rax, [rsp + rbx*8 + 16] ; envp vem após argc + argv[] + ponteiro nulo + mov [envp], rax ; salva ponteiro de envp +; ---------------------------------------------------------- +; Verificar se argc == 2 +; ---------------------------------------------------------- + cmp qword [argc], 2 + jl show_usage ; se argc < 2, mostrar uso correto +; ---------------------------------------------------------- +; Obter endereço do nome da variável: argv[1] +; ---------------------------------------------------------- + mov rax, [argv] + mov rsi, [rax + 8] ; rsi = argv[1] + ; ------------------------------------------ + ; Iterar sobre envp para buscar a variável + ; ------------------------------------------ + mov rcx, [envp] ; rcx recebe endereço de envp[0] +.next_env: + mov rdx, [rcx] ; rdx recebe endereço da string "VAR=valor" + test rdx, rdx ; verifica se rdx = 0 + jz not_found ; se rdx = 0, termina com erro (não encontrada) + ; ------------------------------------------ + ; Comparar caractere a caractere: se argv[1] é prefixo de rdx + ; e é seguido por '=' → variável encontrada + ; ------------------------------------------ + mov r8, rsi ; r8 = nome buscado (argv[1]) + mov r9, rdx ; r9 = string atual do envp +.cmp_loop: + mov al, [r8] ; próximo char do nome buscado + mov bl, [r9] ; próximo char da string do envp + cmp al, 0 + je .check_equals ; se al = 0, chegou ao fim do nome buscado: checar '=' + cmp al, bl + jne .next ; se al != bl, não bateu, próxima variável + inc r8 + inc r9 + jmp .cmp_loop + ; ------------------------------------------ + ; Comparar se o caractere no endereço é '=' + ; ------------------------------------------ +.check_equals: + cmp byte [r9], '=' ; verifica se o caractere em envp é '=' + jne .next ; não era uma '=': próximo elemento em envp + inc r9 ; avança para o início do valor + mov rsi, r9 ; rsi = endereço do primeiro byte do valor + mov rdi, 1 ; stdout + call print_string ; imprime valor + mov rsi, newline ; carrega endereço de '\n' em rsi + call print_string ; imprime \n + mov rdi, 0 ; estado de término 0 (sucesso) + call exit_program ; termina o programa +.next: + add rcx, 8 ; avança para próximo emdereço em envp + jmp .next_env +; ---------------------------------------------------------- +; Caso variável não seja encontrada +; ---------------------------------------------------------- +not_found: + lea rsi, [rel msg_nf] + mov rdi, 2 ; stderr + call print_string + mov rdi, 1 ; código de erro + call exit_program +; ---------------------------------------------------------- +; Caso uso incorreto (menos de 2 argumentos) +; ---------------------------------------------------------- +show_usage: + lea rsi, [rel msg_usage] ; endereço da mensagem de uso + mov rdi, 2 ; fd 2 = stderr + call print_string ; imprime a mensagem + mov rdi, 1 ; estado de término 1 (erro) + call exit_program ; termina o programa +; ---------------------------------------------------------- +; SUB-ROTINAS +; ---------------------------------------------------------- +print_string: +; ---------------------------------------------------------- +; Imprime string terminada em '\0' no descritor indicado +; Entrada: +; rdi = file descriptor (1 = stdout, 2 = stderr) +; rsi = ponteiro para string terminada em '\0' +; ---------------------------------------------------------- + push rdi ; salva rdi (fd) + mov rax, rsi + xor rcx, rcx ; contador = 0 +.count: + cmp byte [rax + rcx], 0 ; fim da string? + je .write + inc rcx + jmp .count +.write: + mov rdx, rcx ; número de bytes a escrever + pop rax ; restaura fd original + mov rdi, rax + mov rax, 1 ; syscall: write + syscall + ret +; ---------------------------------------------------------- +exit_program: +; ---------------------------------------------------------- +; Encerra o programa com código em rdi +; Entrada: +; rdi = código de saída (0 = sucesso, 1 = erro) +; ---------------------------------------------------------- + mov rax, 60 ; syscall: exit + syscall +#+end_src + +Montando e executando, nós teremos algo como: + +#+begin_example +:~$ nasm -f elf64 getenv.asm +:~$ ld -o getenv getenv.o +:~$ ./getenv USER +blau +:~$ ./getenv +Usage: ./getenv VAR +:~$ ./getenv TESTE +Not found +:~$ TESTE=123 ./getenv TESTE +123 +#+end_example + +* Implementação com funções da linguagem C + +As funções da =glibc= abstraem quase todo o trabalho que tivemos com o Assembly, +como podemos ver no exemplo a seguir. + +Arquivo: [[exemplos/07/cgetenv.asm][cgetenv.asm]] + +#+begin_src asm :tangle exemplos/07/cgetenv.asm +; ---------------------------------------------------------- +; Montagem: nasm -f elf64 cgetenv.asm +; Ligação : gcc -no-pie -o cgetenv cgetenv.o +; Uso : ./cgetenv PATH +; ---------------------------------------------------------- +; Funções importadas da glibc... +; ---------------------------------------------------------- +extern getenv +extern puts +extern fprintf +extern exit +extern stderr +; ---------------------------------------------------------- +section .rodata +; ---------------------------------------------------------- + msg_nf db "Not found", 10, 0 + msg_usage db "Usage: ./getenv VAR", 10, 0 +; ---------------------------------------------------------- +section .bss +; ---------------------------------------------------------- + argc resq 1 ; recebe quantidade de argumentos + argv resq 1 ; recebe endereço de argv[0] + envp resq 1 ; recebe endereço de envp[0] +; ---------------------------------------------------------- +section .text +; ---------------------------------------------------------- +global main +main: +; ---------------------------------------------------------- +; Iniciar quadro de pilha para main... +; ---------------------------------------------------------- + push rbp + mov rbp, rsp +; ---------------------------------------------------------- +; Salvar dados de argumentos e ambiente... +; ---------------------------------------------------------- + mov [argc], rdi ; salvar valor de argc + mov [argv], rsi ; salvar endereço de argv[0] + mov [envp], rdx ; salvar endereço de envp[0] +; ---------------------------------------------------------- +; Verificar se argc < 2 +; ---------------------------------------------------------- + cmp rdi, 2 + jl show_usage +; ---------------------------------------------------------- +; getenv(argv[1]); +; ---------------------------------------------------------- + mov rax, [argv] ; endereço de argv[0] + mov rdi, [rax + 8] ; argumento 1: endereço de argv[1] + call getenv ; chamar getenv + test rax, rax ; testar retorno (0 = não encontrada) + jz not_found +; ---------------------------------------------------------- +; Imprimir valor encontrado: puts(char *valor) +; ---------------------------------------------------------- + mov rdi, rax ; argumento 1: endereço do valor em rax + call puts ; chamar puts +; ---------------------------------------------------------- +; Término com sucesso: exit(int status); +; ---------------------------------------------------------- + mov edi, 0 ; argumento 1: (int)0 - sucesso + call exit ; chamar exit +; ---------------------------------------------------------- +; Se o nome da variável não for encontrado... +; ---------------------------------------------------------- +not_found: +; ---------------------------------------------------------- +; Mensagem de erro: fprintf(stderr, "Not found\n"); +; ---------------------------------------------------------- + mov rdi, [stderr] ; argumento 1: stream de saída + lea rsi, [msg_nf] ; argumento 2: string de formato + call fprintf ; chamar fprintf + jmp exit_error ; termina com erro +; ---------------------------------------------------------- +; Quantidade de argumentos incorrerta... +; ---------------------------------------------------------- +show_usage: +; ---------------------------------------------------------- +; Mensagem de erro: fprintf(stderr, "Usage: ./getenv VAR\n"); +; ---------------------------------------------------------- + mov rdi, [stderr] ; argumento 1: stream de saída + lea rsi, [msg_usage] ; argumento 2: string de formato + call fprintf ; chamar fprintf +; ---------------------------------------------------------- +; Término com erro +; ---------------------------------------------------------- +exit_error: + mov edi, 1 ; argumento 1: (int)1 - erro + call exit ; chamar exit +#+end_src + +Montagem, compilação e testes: + +#+begin_example +:~$ nasm -f elf64 cgetenv.asm +:~$ gcc -no-pie -o cgetenv cgetenv.o +:~$ ./cgetenv USER +blau +:~$ ./cgetenv +Usage: ./getenv VAR +:~$ ./cgetenv TESTE +Not found +:~$ TESTE=123 ./cgetenv TESTE +123 +#+end_example + +* Exercícios propostos + +1. Utilizando o GDB, execute os programas de exemplo buscando pela variável + exportada =LANG= e examine as strings obtidas até que a ela seja encontrada. +2. No exemplo =cgetenv.asm=, comente a inicialização da pilha, observe o + resultado e explique por que ele acontece. +3. Crie o programa =salve.asm= para imprimir =Salve, !=, onde =nome= pode ser + recebido tanto como um argumento quanto pela exportação da variável =NOME=. + +* Referências + +- =man 7 environ= +- =man 3 getenv= +- [[https://www.gnu.org/software/libc/manual/html_node/Environment-Variables.html][GNU libc: Environment Variables]] +- [[https://wiki.osdev.org/Calling_Conventions][OSDev: Calling Conventions]]