conteúdo da aula 7

This commit is contained in:
Blau Araujo 2025-05-26 08:04:43 -03:00
parent c142033bf7
commit d99402393f
2 changed files with 460 additions and 4 deletions

View file

@ -346,7 +346,7 @@ _start:
o resultado em um registrador de destino. o resultado em um registrador de destino.
#+end_quote #+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 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 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 módulo de sub-rotinas para conversão e impressão que só será estudada nos
próximos tópicos. próximos tópicos.
** Exemplo híbrido (Assembly+C) ** Exemplo em Assembly com main e gcc
Arquivo: [[exemplos/06/cargs.asm][cargs.asm]] 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 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. chamada a salvar e restaurar seus conteúdos.
** Exemplo em Assembly puro ** Exemplo com acesso direto à pilha
- Arquivo: [[exemplos/06/args.asm][args.asm]] - Arquivo: [[exemplos/06/args.asm][args.asm]]
- Módulo: [[exemplos/06/print_utils.asm][print_utils.asm]] - Módulo: [[exemplos/06/print_utils.asm][print_utils.asm]]
@ -640,7 +640,7 @@ argv[4]: d
* Exercícios propostos * Exercícios propostos
1. Crie um programa em Assembly que receba seu nome como argumento e imprima =Salve, NOME!=. 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=. 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. 4. Com base nos exemplos do tópico, crie um programa que liste as variáveis no vetor de ambiente.

456
curso/aula-07.org Normal file
View file

@ -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, <nome>!=, 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]]