diff --git a/README.org b/README.org index c43fbe7..04c60fc 100644 --- a/README.org +++ b/README.org @@ -29,3 +29,4 @@ qualquer distribuição. - 21.03.2025 [[./aulas/05-controle/README.org][Aula 5: Estruturas de controle de fluxo]] ([[https://youtu.be/9dvDL7FbYKY][vídeo]]) ([[./exercicios/05/README.org][exercícios]]) - 26.03.2025 [[./aulas/06-vetores/README.org][Aula 6: Vetores]] ([[https://youtu.be/W5TGNQYFs4E][vídeo]]) ([[./exercicios/06/README.org][exercícios]]) - 28.03.2025 [[./aulas/07-vps/README.org][Aula 7: Vetores, ponteiros e strings]] ([[https://youtu.be/hhySl3ClTLE][vídeo]]) ([[./exercicios/07/README.org][exercícios]]) +- 31.03.2025 [[./aulas/08-processos/README.org][Aula 8: Processos e layout de memória]] ([[https://youtu.be/60bXYVCFoTI][vídeo]]) (sem exercícios) diff --git a/aulas/08-processos/README.org b/aulas/08-processos/README.org new file mode 100644 index 0000000..e6ee407 --- /dev/null +++ b/aulas/08-processos/README.org @@ -0,0 +1,421 @@ +#+title: Curso Básico da Linguagem C +#+subtitle: Aula 8: Processos +#+author: Blau Araujo +#+startup: show2levels +#+options: toc:3 + +* Aula 8: Processos e layout de memória + +[[https://youtu.be/60bXYVCFoTI][Vídeo desta aula]] + +** Filosofia UNIX + +Doug McIlroy, que implementou o conceito de /pipes/ no Unix, resumiu a /filosofia +UNIX/ em três princípios: + +- /Escreva programas que façam apenas uma coisa, mas que a façam bem feita./ +- /Escreva programas que trabalhem juntos./ +- /Escreva programas que manipulem fluxos de texto, pois esta é uma interface universal./ + +Se repararmos bem, esses princípios de caracterizam perfeitamente o +desenvolvimento do sistema operacional Unix e a forma como ele foi projetado +para ser operado. + +*Lembre-se...* + +Os sistemas operacionais parecidos com o Unix (/Unix-like/) podem ser descritos +através de seus quatro conjuntos de softwares mais essenciais, o que também se +aplica ao nosso GNU/Linux: + +| Componente | Unix/Unix-like | GNU/Linux | +|--------------------------------+----------------------------------------+---------------| +| Kernel | Unix Kernel, BSD Kernel, etc... | Linux | +| Biblioteca C padrão | =libc= | =glibc= | +| Shell | =sh= | =bash= | +| Utilitários da base do sistema | Programas como: =cat=, =grep=, =sed=, etc... | GNU Coreutils | + +*** Primeiro princípio: especialização + +#+begin_quote +/Escreva programas que façam apenas uma coisa, mas que a façam bem feita./ +#+end_quote + +*Separação de atribuições...* + +No escopo do sistema como um todo, nós podemos relacionar esse princípio +com a separação entre o /espaço do kernel/ e o /espaço de usuário/: + +- *Espaço do kernel:* região de memória onde o kernel do sistema operacional + executa e tem acesso privilegiado a todos os recursos de hardware e software. + +- *Espaço do usuário:* região de memória onde os /processos/ dos programas são + executados com permissões restritas e têm o acesso ao hardware intermediado + pelo kernel. + +*Criação de programas...* + +Para a escrita de programas, nós temos a =libc=, que abstrai as chamadas às +funções internas do kernel (/chamadas de sistema/, ou /syscalls/) e centenas +de outras funcionalidades. + +*Interface padrão de operação...* + +Mesmo no espaço de usuário, o /shell/ se diferencia dos demais programas da +base do sistema porque, em princípio, é somente através dele que os outros +programas são executados pelo /usuário/. + +#+begin_quote +O /shell/ é a interface padrão entre o sistema e o usuário que, através de linhas +de comandos em texto, declara o que quer que seja executado. +#+end_quote + +*Programas para diversas tarefas úteis...* + +Finalmente, o usuário tem acesso a dezenas de utilitários da base do sistema, +cada um com a sua especialidade, para a realização de inúmeros tipos de +tarefas. + +*** Segundo princípio: modularidade + +#+begin_quote +/Escreva programas que trabalhem juntos./ +#+end_quote + +Com a especialização, vem a necessidade de fazer com que cada componente do +sistema, de acordo com suas atribuições, seja projetado para trabalhar com +outros programas. + +*Linha de comandos...* + +Para operar o sistema, o usuário tem acesso a um terminal onde seus comandos +poderão ser digitados e enviados para o shell: + +#+begin_example + +---------+ +----------+ +---------+ +--------+ + | TECLADO | ---→ | | ---→ | SHELL | ---→ | KERNEL | + +---------+ | | +---------+ +--------+ + | TERMINAL | ↑ + +---------+ | | +---------------+ | + | MONITOR | ←--- | | ←--→ | NOVO PROCESSO | ←--+ + +---------+ +----------+ +---------------+ +#+end_example + +Quando o shell recebe a linha de um comando, ele interpreta o que foi +digitado e, se for o caso, faz uma /chamada de sistema/ =fork= para que +o kernel crie uma cópia (/clone/) de seu processo. Em seguida, no processo +clonado, o shell faz uma chamada de sistema =exec=, para que o kernel +substitua parte dos dados copiados do processo do shell pelos dados +do programa que será executado. + +*Tarefas complexas...* + +Cada utilitário da base do sistema é construído segundo o conceito de +/interface de linha de comando/, ou CLI. Isso quer dizer que, além das +suas especialidades, cada um deles é capaz de trocar dados com outros +programas para que o usuário, a partir de programas simples, seja +capaz de realizar tarefas complexas. + +Por exemplo, digamos que você tenha um arquivo com uma lista de +pedidos e queira filtrar as compras realizadas em um dado mês. Você +poderia utilizar o programa =grep=, que é especializado em localizar +e imprimir linhas de texto a partir de padrões descritos por +expressões regulares: + +#+begin_example +:~$ grep '03/2025' pedidos.data +"Maria das Couves", "02/03/2025", "2564", "Porta copos" +"Antônio dos Santos", "09/03/2025", "7544", "Toalha de mesa" +"João da Silva", "01/03/2025", "3762", "Jogo de 12 talheres" +#+end_example + +Se, além das linhas impressas, você precisar ordenar o resultado +pelos nomes dos clientes, o =grep= será insuficiente sozinho, mas +você pode recorrer ao utilitário =sort= para processar a saída +produzida pelo =grep= escrevendo apenas um comando: + +#+begin_example +:~$ grep '03/2025' pedidos.data | sort +"Antônio dos Santos", "09/03/2025", "7544", "Toalha de mesa" +"João da Silva", "01/03/2025", "3762", "Jogo de 12 talheres" +"Maria das Couves", "02/03/2025", "2564", "Porta copos" +#+end_example + +*** Terceiro princípio: fluxos de texto + +#+begin_quote +/Escreva programas que manipulem fluxos de texto, pois esta é uma interface universal./ +#+end_quote + +Como vimos, até aqui... + +- 0 terminal recebe um fluxo de caracteres digitado em um teclado e envia + para o shell; +- O shell interpreta o texto recebido e monta textos correspondendo aos + argumentos que serão passados para as chamadas de sistema; +- O kernel executa a chamada de sistema e cria um novo processo para + executar o programa que foi invocado... Mas não é só isso! + +Além dos dados do programa (basicamente, o conteúdo de seu binário), +o novo processo incluirá outros dados que já estavam registrados no +processo do shell: + +- A quantidade de palavras utilizadas para invocar o programa; +- A lista das palavras utilizadas para invocar o programa; +- Uma lista das variáveis que serão herdadas pelo novo processo; +- Uma lista de arquivos padrão que poderão ser utilizados para enviar + e receber fluxos de texto. + +Sendo assim... + +- As palavras utilizadas para invocar o programa são chamadas de /argumentos + de linha de comando/. +- A lista de variáveis herdadas pelo programa irão compor um /ambiente de + dados/ que poderão ser utilizados pelo programa. +- A lista de arquivos padrão para receber e enviar fluxos de caracteres para + o terminal são os /descritores de arquivos padrão/: =stdin= (entrada padrão), + =stdout= (saída padrão) e =stderr= (saída padrão de erros). + +Mas existem mecanismos no kernel que possibilitam o desvio dos fluxos de +dados padrão para outros arquivos. Assim, se eu quiser enviar a saída do +meu programa para um arquivo, em vez de para o terminal, eu posso redirecionar +a saída padrão (=stdout=, descritor de arquivos =1=) para esse arquivo: + +#+begin_example +ls -l > arquivos.txt +#+end_example + +Do mesmo modo, eu poderia desviar a saída padrão de um programa para a +entrada padrão de outro programa. Para isso, o kernel precisaria criar +um outro tipo de arquivo, chamado de /pipe/, para canalizar o fluxo de +texto entre os dois programas. Na linha de comandos... + +#+begin_example +:~$ grep '03/2025' pedidos.data | sort +#+end_example + +Nesse caso, os dois programas são executados ao mesmo tempo e, enquanto +o primeiro envia dados para um /pipe/, o segundo lê este mesmo arquivo: + +#+begin_example + +-----------------------------+ +------+ +------+ + | grep '03/2025' pedidos.data | ---> | PIPE | ---> | sort | + +-----------------------------+ +------+ +------+ +#+end_example + +Assim que o =grep= terminar o envio de linhas de texto, o =sort= será terminado. + +** A interface de linha de comando (CLI) + +A filosofia Unix nos ajuda a compreender, de um modo mais amplo, como os +sistemas /Unix-like/ foram pensados para lidar com o hardware, possibilitarem +a criação e execução de programas e serem operados. Especialmente quanto à +operação do sistema pela linha de comandos, são as convenções da /interface +de linha de comandos/ (CLI), implementada no shell, que determinam como +os programas e o sistema deverão lidar com os nossos comandos. + +Um comando simples, seque este esquema geral: + +#+begin_example +[EXPORTAÇÕES] [INVOCAÇÃO] [ARGS...] [REDIRECIONAMENTO ARQUIVO] +#+end_example + +- *Exportações:* uma ou mais variáveis que serão exportadas para o ambiente + do processo do programa executado. +- *Invocação:* o caminho e o nome do programa que será executado. +- *Argumentos:* lista de palavras que serão passadas para o processo do + programa como opções ou informações adicionais. +- *Redirecionamento:* através de operadores do shell, a saída do programa + pode ser desviada para um arquivo ou um arquivo poderá ser informado + para ser lido e processado pelo programa. + +#+begin_quote +A /invocação/ (o caminho e o nome do programa) sempre será o primeiro argumento +da lista de argumentos. +#+end_quote + +*** Operadores de controle do shell + +Quando a linha de um comando contém a invocação de mais de um programa, +é necessário estabelecer como essas invocações se relacionam, o que é +feito com os /operadores de controle/ do shell: + +- *Encadeamento incondicional* (=;= ou =\n=): O comando seguinte será executado após o + término do anterior. + +- *Encadeamento assíncrono* (=&=): O comando seguinte será executado em paralelo com + o anterior e em segundo plano. + +- *Encadeamento condicional "se sucesso"* (=&&=): O comando seguinte só será + executado se, e quando, o último comando executado terminar com sucesso. + +- *Encadeamento condicional "se erro"* (=||=): O comando seguinte só será executado + se, e quando, o último comando executado terminar com erro. + +- *Encadeamento por pipe* (=|=): Os comandos serão executados em paralelo e a saída + do primeiro será canalizada para a entrada do segundo. + +** O que são processos + +Um processo é um conjunto de estruturas de dados que o kernel utiliza para +gerenciar a execução de programas. No centro dessas estruturas de dados está uma +faixa virtual contínua de endereços de memória que o kernel designará para cada +programa em execução. Essa faixa virtual de endereços de memória, também chamada +de /memória virtual/ ou /espaço de endereços/, representa toda a memória disponível +a que o processo do programa terá acesso. + +Além disso, o kernel disponibiliza todas as informações sobre todos os processos +em execução na forma de um sistema de arquivos virtual (/procfs/) montado no +diretório =/proc=. Nele, cada processo terá um subdiretório nomeado segundo seu +número de identificação (PID). + +** Como programas são executados + +Para o sistema operacional, /executar um programa/ significa criar um novo processo +e designar um espaço de memória para ele. Para chegar a este ponto, o programa terá +que ser invocado por outro programa já em execução (/processo pai/, geralmente o shell) +que, através de duas chamadas de sistema responsáveis por (chamada =fork=) criar uma +duplicata do processo pai (um /clone/) como um novo processo e depois substituir parte +dos dados no espaço de memória dessa duplicata pelos dados encontrados no binário +do programa que será executado. + +Aqui está um exemplo em C que mostra. simplificadamente, a execução de outro programa: + +#+begin_src c +#include +#include +#include +#include +#include + +int main(void) { + // Chamada fork... + pid_t pid = fork(); + + // Se o fork falhar... + if (pid < 0) { + perror("fork falhou"); + exit(EXIT_FAILURE); + } + + /* + Um segundo processo é criado e o código seguinte será executado + por ambos os processos (eles são idênticos!), mas com valores + de pid diferentes: + + - Para o processo filho, pid == 0 + - Para o processo pai, pid == PID do processo filho + + Por isso temos essa estrutura if... + */ + if (pid == 0) { + // Processo filho... + printf("[Filho] PID: %d, iniciado.\n", getpid()); + char *args[] = {"ls", "-l", NULL}; + // Chamada exec (execvp)... + execvp(args[0], args); + // Se execvp falhar... + perror("execvp falhou"); + exit(EXIT_FAILURE); + } else { + // Processo pai... + printf("[Pai] PID: %d, criou filho PID: %d\n", getpid(), pid); + printf("[Pai] Aguardando término do filho...\n"); + int status; + waitpid(pid, &status, 0); + printf("[Pai] Processo filho %d terminou com status %d\n", pid, WEXITSTATUS(status)); + printf("[Pai] Voltando ao controle.\n"); + } + + return 0; +} +#+end_src + +Compilando e executando: + +#+begin_example +:~$ gcc -Wall exemplo.c +:~$ ./a.out +[Pai] PID: 1810185, criou filho PID: 1810186 +[Pai] Aguardando término do filho... +[Filho] PID: 1810186, iniciado. +total 56 +-rwxrwxr-x 1 blau blau 16280 mar 22 08:00 analise +-rw-rw-r-- 1 blau blau 1549 mar 22 08:05 analise.c +-rwxrwxr-x 1 blau blau 16304 mar 30 11:37 a.out +-rw-rw-r-- 1 blau blau 594 mar 22 11:22 exemplo.c +-rw-rw-r-- 1 blau blau 325 mar 22 10:45 fizzbuzz.c +-rw-rw-r-- 1 blau blau 1743 mar 17 21:32 limites.c +-rw-rw-r-- 1 blau blau 340 mar 20 14:33 str.c +-rw-rw-r-- 1 blau blau 882 mar 30 11:37 teste.c +[Pai] Processo filho 1810186 terminou com status 0 +[Pai] Voltando ao controle. +#+end_example + +#+begin_quote +No GNU/Linux, o shell chama a /syscall/ =clone=, em vez de =fork=, e =execve=, +em vez de =execvp=. +#+end_quote + +** Layout de memória + +Quando o processo é iniciado, ele recebe uma faixa contínua de endereços /virtuais/ +de memória, segundo o layout abaixo: + +#+begin_example + ENDEREÇOS MAIS ALTOS + +--------------------------+ ---+ + | Vetor Ambiente | | + +--------------------------+ | + | Vetor Argumentos | | + +--------------------------+ PILHA (STACK) + | Quantidade de argumentos | | + +--------------------------+ | + | Dados das funções | | + +------------+-------------+ ---+ + | ↓ | + | | + | ↑ | + +------------+-------------+ + | | <- Mapeamento de arquivos. + | HEAP | <- Bibliotecas dinâmicas. + | | <- Alocação dinâmica. + +--------------------------+ + | .bss | Dados globais e estáticos não inicializados. + +--------------------------+ + | .data | Dados globais e estáticos inicializados. + +--------------------------+ + | .rodata | Dados constantes (read only). + +--------------------------+ + | .text | Código do programa. + +--------------------------+ + ENDEREÇOS MAIS BAIXOS +#+end_example + +*** Conteúdo do binário executável + +As seções do binário do executável serão copiadas para os segmentos +mais baixos desse espaço de endereços. + +*** Região do HEAP + +Acima dos dados do binário, uma grande região é designada para a alocação +dinâmica de espaços em memória para receber dados processados durante a execução +do programa: o /heap/. Nesta mesma região, também são carregados os conteúdos +binários das bibliotecas carregadas dinamicamente, como a =glibc=, o =ld-linux= e +a biblioteca =vdso=, do kernel. + +*** Região da pilha (stack) + +Nos endereços mais altos, é configurada uma estrutura de dados chamada /pilha/. +Como o nome sugere, é uma estrutura onde os dados são "empilhados" uns sobre os +outros, como numa pilha de pratos. + +Na base da pilha, nós encontramos um vetor de strings contendo as variáveis +exportadas para o processo (vetor ambiente). Imediatamente acima, nós temos +outro vetor de strings com as palavras utilizadas na linha do comando para +invocar a execução do programa e, eventualmente, seus argumentos (vetor de +argumentos ou /parâmetros/). Por último, no topo da pilha, nós encontraremos +a quantidade de palavras no vetor de argumentos. + +Ao longo da execução do programa, os dados das funções que forem chamadas +serão incluídos no topo da pilha e serão removidos quando elas terminarem. diff --git a/aulas/08-processos/a.out b/aulas/08-processos/a.out new file mode 100755 index 0000000..1854ff7 Binary files /dev/null and b/aulas/08-processos/a.out differ diff --git a/aulas/08-processos/fork-exec.c b/aulas/08-processos/fork-exec.c new file mode 100644 index 0000000..89f7a12 --- /dev/null +++ b/aulas/08-processos/fork-exec.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +int main(void) { + // Chamada fork... + pid_t pid = fork(); + + // Se o fork falhar... + if (pid < 0) { + perror("fork falhou"); + exit(EXIT_FAILURE); + } + + /* + Um segundo processo é criado e o código seguinte será executado + por ambos os processos (eles são idênticos!), mas com valores + de pid diferentes: + + - Para o processo filho, pid == 0 + - Para o processo pai, pid == PID do processo filho + + Por isso temos essa estrutura if... + */ + if (pid == 0) { + // Processo filho... + printf("[Filho] PID: %d, iniciado.\n", getpid()); + char *args[] = {"ls", "-l", NULL}; + // Chamada exec (execvp)... + execvp(args[0], args); + // Se execvp falhar... + perror("execvp falhou"); + exit(EXIT_FAILURE); + } else { + // Processo pai... + printf("[Pai] PID: %d, criou filho PID: %d\n", getpid(), pid); + printf("[Pai] Aguardando término do filho...\n"); + int status; + waitpid(pid, &status, 0); + printf("[Pai] Processo filho %d terminou com status %d\n", pid, WEXITSTATUS(status)); + printf("[Pai] Voltando ao controle.\n"); + } + + return 0; +} diff --git a/aulas/08-processos/mmap/#main.c# b/aulas/08-processos/mmap/#main.c# new file mode 100644 index 0000000..cb4ca46 --- /dev/null +++ b/aulas/08-processos/mmap/#main.c# @@ -0,0 +1,99 @@ +#include +#include +#include +#include + + +#define LINE_LEN 256 // Quantidade de bytes para as linhas. +#define PATH_LEN 256 // Quantidade de bytes para os caminhos. +#define PERM_LEN 5 // Quantidade de bytes para as permissões. +#define PROG_END 5 // Fim das linhas do programa. + +typedef unsigned long u64t; +typedef int i32t; + +void read_maps(void); + + +int main(void) { + read_maps(); + return 0; +} + + +/* + * Lê e imprime o conteúdo de /proc/self/maps filtrando + * as faixas dos segmentos de memória. + */ +void read_maps(void) { + // Define um descritor de arquivos para ler /proc/self/maps... + FILE *fd = fopen("/proc/self/maps", "r"); + + // Terminar no caso de erro... + if (!fd) { + perror("Erro ao abrir /proc/self/maps"); + exit(EXIT_FAILURE); + } + + char line[LINE_LEN]; // Linhas lidas do arquivo. + u64t start; // Endereço inicial. + u64t end; // Endereço final. + char perm[PERM_LEN]; // Permissões. + char path[PATH_LEN]; // Caminho do arquivo carregado. + i32t current_line = 1; // Linha atual. + + // Itera as linhas de /proc/self/maps... + while (fgets(line, sizeof(line), fd)) { + + // Zera a string em 'path'... + path[0] = '\0'; + + /* + * Analisa a linha conforme os campos de /proc/self/maps: + * ADDR_START-ADDR_END PERM FILE_OFFSET DEVICE INODE FILE_PATH + */ + sscanf(line, "%lx-%lx %4s %*s %*s %*s %255[^\n]", &start, &end, perm, path); + + // Impressão dos segmentos do código... + if (current_line <= PROG_END) { + printf("0x%lx-0x%lx %s --> ", start, end, perm); + switch (current_line) { + case 1: + puts("Tabela de cabeçalhos do programa"); + break; + case 2: + puts("Código do programa (.text)"); + break; + case 3: + puts("Dados constantes (.rodata)"); + break; + case 4: + puts("Tabela de dados globais e dinâmicos"); + break; + case 5: + puts("Variáveis globais e estáticas (.data e .bss)"); + break; + } + current_line++; + continue; + } + + // Impressão da faixa do HEAP... + if (strstr(path, "[heap]")) { + printf("0x%lx-0x%lx %s --> [HEAP]\n", start, end, perm); + continue; + } + + // Impressão da faixa da STACK... + if (strstr(path, "[stack]")) { + printf("0x%lx-0x%lx %s --> [STACK]\n", start, end, perm); + continue; + } + + // Demais linhas com caminho... + if (path[0] != '\0') printf("0x%lx-0x%lx %s --> %s\n", start, end, perm, path); + } + + // Fecha o descritor de arquivos... + fclose(fd); +} diff --git a/aulas/08-processos/mmap/a.out b/aulas/08-processos/mmap/a.out new file mode 100755 index 0000000..5f34bee Binary files /dev/null and b/aulas/08-processos/mmap/a.out differ diff --git a/aulas/08-processos/mmap/main.c b/aulas/08-processos/mmap/main.c new file mode 100644 index 0000000..5cf3b1c --- /dev/null +++ b/aulas/08-processos/mmap/main.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include + +#define LINE_LEN 256 // Quantidade de bytes para as linhas. +#define PATH_LEN 256 // Quantidade de bytes para os caminhos. +#define PERM_LEN 5 // Quantidade de bytes para as permissões. +#define PROG_END 5 // Fim das linhas do programa. + +typedef unsigned long u64t; +typedef int i32t; + +void read_maps(); + +int main(void) { + read_maps(); + return 0; +} + +/* + * Lê e imprime o conteúdo de /proc/self/maps filtrando + * as faixas dos segmentos de memória. + */ +void read_maps() { + // Define um descritor de arquivos para ler /proc/self/maps... + FILE *fd = fopen("/proc/self/maps", "r"); + + // Terminar no caso de erro... + if (!fd) { + perror("Erro ao abrir /proc/self/maps"); + exit(EXIT_FAILURE); + } + + char line[LINE_LEN]; // Linhas lidas do arquivo. + u64t start; // Endereço inicial. + u64t end; // Endereço final. + char perm[PERM_LEN]; // Permissões. + char path[PATH_LEN]; // Caminho do arquivo carregado. + i32t current_line = 1; // Linha atual. + + // Itera as linhas de /proc/self/maps... + while (fgets(line, sizeof(line), fd)) { + + // Zera a string em 'path'... + path[0] = '\0'; + + /* + * Analisa a linha conforme os campos de /proc/self/maps: + * ADDR_START-ADDR_END PERM FILE_OFFSET DEVICE INODE FILE_PATH + */ + sscanf(line, "%lx-%lx %4s %*s %*s %*s %255[^\n]", &start, &end, perm, path); + + // Impressão dos segmentos do código... + if (current_line <= PROG_END) { + printf("0x%lx-0x%lx %s --> ", start, end, perm); + switch (current_line) { + case 1: + puts("Tabela de cabeçalhos do programa"); + break; + case 2: + puts("Código do programa (.text)"); + break; + case 3: + puts("Dados constantes (.rodata)"); + break; + case 4: + puts("Tabela de dados globais e dinâmicos"); + break; + case 5: + puts("Variáveis globais e estáticas (.data e .bss)"); + break; + } + current_line++; + continue; + } + + // Impressão da faixa do HEAP... + if (strstr(path, "[heap]")) { + printf("0x%lx-0x%lx %s --> [HEAP]\n", start, end, perm); + continue; + } + + // Impressão da faixa da STACK... + if (strstr(path, "[stack]")) { + printf("0x%lx-0x%lx %s --> [STACK]\n", start, end, perm); + continue; + } + + // Demais linhas com caminho... + if (path[0] != '\0') printf("0x%lx-0x%lx %s --> %s\n", start, end, perm, path); + } + + // Fecha o descritor de arquivos... + fclose(fd); +} diff --git a/aulas/08-processos/old/README.org b/aulas/08-processos/old/README.org new file mode 100644 index 0000000..e79c5d4 --- /dev/null +++ b/aulas/08-processos/old/README.org @@ -0,0 +1,229 @@ +#+title: Curso Básico da Linguagem C +#+subtitle: Aula 4: Layout de memória +#+author: Blau Araujo +#+startup: show2levels +#+options: toc:3 + +* Aula 4: Layout de memória + +- [[][Vídeo desta aula]] + +** Introdução + +No fundo, esta é uma aula sobre variáveis. Mas, principalmente para quem está +iniciando na programação em C, dizer que variáveis são como gavetas onde nós +guardamos e acessamos dados aleatoriamente é quase a mesma coisa que tentar +formar cirurgiões com o Jogo da Operação. + +#+CAPTION: Jogo da Operação +[[./jogo-taz.png]] + +Isso funciona muito bem com crianças em alfabetização, mas não com quem está +se preparando para assumir responsabilidades programando numa linguagem que +deixa por conta da pessoa que programa todos os cuidados com as potenciais +falhas e vulnerabilidades que podem por em risco, não apenas o programa que +está sendo escrito, como também todo o sistema em que ele será executado. + +Então, nós podemos definir variáveis como elementos da linguagem que serão +associados a valores manipuláveis na memória, podemos dizer que esses valores +serão dimensionados conforme seus tipos (assunto da aula passada), que eles +serão encontrados em endereços específicos na memória... Enfim, mas nada +disso fará sentido, nem dará uma noção realista das implicações do poder que +nós temos em mãos quando somos autorizados, pela linguagem C, a manipular +quase que livremente o espaço de memória. + +Em grande parte, é a falta dessa noção que nos leva a situações de risco, +como (se prepare para o inglês): + +- Heap Overflow +- Stack Overflow +- Buffer Overflow +- Use-After-Free (UAF) +- Double Free +- Dangling Pointer +- Memory Leak +- Uninitialized Memory Access +- Out-of-Bounds Read/Write +- Null Pointer Dereference +- Stack Corruption +- Heap Corruption +- Race Conditions + +E depois, vão dizer que a linguagem C é insegura, propensa a vulnerabilidades +de memória... E por aí vai. Sim, a linguagem C não tem mecanismos que nos +impeçam de cometer erros, mas não é ela que comete os erros. + +Por isso, este talvez seja o vídeo mais longo do nosso curso. Nele, nós vamos +demonstrar como o sistema operacional, o nosso GNU/Linux, lida com a execução +de programas, especificamente no que diz respeito ao espaço de memória que é +disponibilizado para eles. + +** Processos e memória + +- O kernel gerencia a execução de programas através de /processos/. +- Processos são estruturas de dados associadas a cada um dos programas + que estão sendo executados. +- Uma parte central dessa estrutura de dados é o /layout de memória/, que + é uma faixa de endereços mapeada pelo sistema para que os programas possam + acessar a memória através de endereços adjacentes. + +#+begin_quote +Essa faixa de endereços é /virtual/ porque são endereços que não correspondem +aos endereços reais da memória física e, por isso mesmo, os programas terão +acesso a uma faixa contínua de endereços em vez de localizações espalhadas +e dispersas ao longo dos endereços reais da memória. +#+end_quote + +** O espaço de endereços (layout de memória) + +A faixa de endereços atribuída a um processo é dividida em vários /segmentos +de memória/ com finalidades específicas. + +#+caption: Layout de memória +[[./mem-layout.png]] + + +*** Dados copiados do binário do programa + +Nos endereços mais baixos da memória virtual, serão copiados os dados +presentes nas /seções/ dos binários dos nossos programas: + +- =.text=: O conteúdo executável do programa (código). +- =.rodata=: Dados constantes. +- =.data=: Dados globais e estáticos inicializados. +- =.bss=: Dados globais e estáticos não inicializados. + +*** Dados dinâmicos + +A região intermediária dos endereços mapeados, chamada de /heap/, é reservada +ao uso com: + +- Dados que requeiram espaços alocados dinamicamente ao longo da execução + do programa (com a função =malloc=, por exemplo). +- Mapeamento do conteúdo de arquivos e grandes volumes de dados. +- Conteúdo de bibliotecas carregadas dinamicamente, como a =glibc=, o + carregador dinâmico (=ld-linux=) e a biblioteca =vdso=, do Linux. + +#+begin_quote +A localização dos dados dinâmicos é aleatória dentro da faixa do /heap/ +que, conforme a necessidade, se expande na direção dos endereços mais +altos, ou seja, em direção à pilha. +#+end_quote + +*** Pilha (stack) + +Os endereços mais altos da memória virtual são reservados à /pilha de +execução/ do programa. Uma /pilha/, ou /stack/, é uma estrutura onde os dados +são, literalmente, empilhados uns sobre os outros. No GNU/Linux, a base +da pilha está no seu endereço mais alto, enquanto que os novos dados serão +empilhados na direção dos endereços mais baixos. + +Ao ser iniciada, a pilha recebe, da sua base para o topo: + +- Lista das variáveis exportadas para o processo (/ambiente/ / /envp/). +- Lista dos argumentos de linha de comando que invocaram o programa (/argv/). +- Um valor inteiro relativo à quantidade de argumentos (/argc/). + +No caso de programas escritos em C, ao serem iniciados, o dado no topo da +pilha, a quantidade de argumentos, é removido e, a partir daí, são +empilhados os dados locais da função =main=, o que inclui: + +- Variáveis declaradas nos parâmetros da função. +- Variáveis declaradas no corpo da função. + +À medida em que o programa é executado, os dados das outras funções +chamadas também serão empilhados até serem removidos após seus respectivos +términos. + +** Resumo do mapeamento de memória + +O kernel expõe diversas informações sobre os processos em execução na +forma de arquivos de texto no diretório virtual =/proc=. Nele, cada processo +terá um diretório e, nesses diretórios, nós encontramos o arquivo =maps=, +que contém uma versão resumida de todas as faixas de endereços mapeados. + +Para visualizar o mapeamento de um processo de número =PID=: + +#+begin_example +cat /proc/PID/maps +#+end_example + +** Programa =memlo.c= + +Para demonstrar como os dados de um programa são mapeados na memória virtual, +nós vamos utilizar o programma =memlo.c=: + +#+begin_src c +#include +#include +#include + +#define APGL_HINT 0x4c475041 +#define BLAU_HINT 0x55414c42 + +int bss_var; +int data_var = APGL_HINT; +const int ro_data = BLAU_HINT; + +int func(void) { + return 42; +} + +int main(int argc, char **argv, char **envp) { + + static int lsni_var; + static int lsi_var = BLAU_HINT; + + int lni_var; + int li_var = APGL_HINT; + + char *str_ptr = "Salve!"; + + void *heap_ptr = malloc(16); + + puts("[stack]"); + printf("%p início do vetor envp (%s)\n", *envp, *envp); + printf("%p início do vetor argv (%s)\n", *argv, *argv); + printf("%p envp ponteiro para envp (%p)\n", envp, *envp); + printf("%p argv ponteiro para argv (%p)\n", argv, *argv); + printf("%p lni_var variável não inicializada\n", &lni_var); + printf("%p li_var variável inicializada\n", &li_var); + printf("%p &str_ptr ponteiro com endereço da string (%p)\n", &str_ptr, "Salve!"); + printf("%p &heap_ptr ponteiro com endereço na heap (%p)\n", &heap_ptr, heap_ptr); + printf("%p argc quantidade de argumentos (%d)\n", &argc, argc); + + puts("[mmap]"); + printf("%p malloc() função da glibc\n", (void *)malloc); + printf("%p printf() função da glibc\n", (void *)printf); + + puts("[heap]"); + printf("%p heap_ptr espaço alocado dinamicamente na heap\n", heap_ptr); + + puts("[.bss]"); + printf("%p lsni_var variável local estática não inicializada\n", &lsni_var); + printf("%p bss_var variável global não inicializada\n", &bss_var); + + puts("[.data]"); + printf("%p lsi_var variável local estática inicializada\n", &lsi_var); + printf("%p data_var variável global inicializada\n", &data_var); + + puts("[.rodata]"); + printf("%p str_ptr endereço de uma string (%s)\n", str_ptr, str_ptr); + printf("%p ro_data constante global\n", &ro_data); + + puts("[.text]"); + printf("%p main() função main\n", (void *)main); + printf("%p func() função func\n", (void *)func); + + free(heap_ptr); + + sleep(300); + + return EXIT_SUCCESS; +} +#+end_src + +#+begin_quote +A análise do programa em si ficará como parte dos execícios desta aula. +#+end_quote + diff --git a/aulas/08-processos/old/a.out b/aulas/08-processos/old/a.out new file mode 100755 index 0000000..5a7f18a Binary files /dev/null and b/aulas/08-processos/old/a.out differ diff --git a/aulas/08-processos/old/jogo-taz.png b/aulas/08-processos/old/jogo-taz.png new file mode 100644 index 0000000..20fcd6c Binary files /dev/null and b/aulas/08-processos/old/jogo-taz.png differ diff --git a/aulas/08-processos/old/mem-layout.png b/aulas/08-processos/old/mem-layout.png new file mode 100644 index 0000000..8ebd5f2 Binary files /dev/null and b/aulas/08-processos/old/mem-layout.png differ diff --git a/aulas/08-processos/old/memlo.c b/aulas/08-processos/old/memlo.c new file mode 100644 index 0000000..74a43f2 --- /dev/null +++ b/aulas/08-processos/old/memlo.c @@ -0,0 +1,67 @@ +#include +#include +#include + +#define APGL_HINT 0x4c475041 +#define BLAU_HINT 0x55414c42 + +int bss_var; +int data_var = APGL_HINT; +const int ro_data = BLAU_HINT; + +int func(void) { + return 42; +} + +int main(int argc, char **argv, char **envp) { + + static int lsni_var; + static int lsi_var = BLAU_HINT; + + int lni_var; + int li_var = APGL_HINT; + + char *str_ptr = "Salve!"; + + void *heap_ptr = malloc(16); + + puts("[stack]"); + printf("%p início do vetor envp (%s)\n", *envp, *envp); + printf("%p início do vetor argv (%s)\n", *argv, *argv); + printf("%p envp ponteiro para envp (%p)\n", envp, *envp); + printf("%p argv ponteiro para argv (%p)\n", argv, *argv); + printf("%p lni_var variável não inicializada\n", &lni_var); + printf("%p li_var variável inicializada\n", &li_var); + printf("%p &str_ptr ponteiro com endereço da string (%p)\n", &str_ptr, "Salve!"); + printf("%p &heap_ptr ponteiro com endereço na heap (%p)\n", &heap_ptr, heap_ptr); + printf("%p argc quantidade de argumentos (%d)\n", &argc, argc); + + puts("[mmap]"); + printf("%p malloc() função da glibc\n", (void *)malloc); + printf("%p printf() função da glibc\n", (void *)printf); + + puts("[heap]"); + printf("%p heap_ptr espaço alocado dinamicamente na heap\n", heap_ptr); + + puts("[.bss]"); + printf("%p lsni_var variável local estática não inicializada\n", &lsni_var); + printf("%p bss_var variável global não inicializada\n", &bss_var); + + puts("[.data]"); + printf("%p lsi_var variável local estática inicializada\n", &lsi_var); + printf("%p data_var variável global inicializada\n", &data_var); + + puts("[.rodata]"); + printf("%p str_ptr endereço de uma string (%s)\n", str_ptr, str_ptr); + printf("%p ro_data constante global\n", &ro_data); + + puts("[.text]"); + printf("%p main() função main\n", (void *)main); + printf("%p func() função func\n", (void *)func); + + free(heap_ptr); + + sleep(300); + + return EXIT_SUCCESS; +} diff --git a/aulas/08-processos/old/var-global.c b/aulas/08-processos/old/var-global.c new file mode 100644 index 0000000..b382999 --- /dev/null +++ b/aulas/08-processos/old/var-global.c @@ -0,0 +1,12 @@ +#include + +int b = 0x5a; + +int main(void) { + int a = 0x58; + + printf("a: %d @ %p\n", a, &a); + printf("b: %d @ %p\n", b, &b); + + return 0; +} diff --git a/aulas/08-processos/old/var-local.c b/aulas/08-processos/old/var-local.c new file mode 100644 index 0000000..0e3e7c9 --- /dev/null +++ b/aulas/08-processos/old/var-local.c @@ -0,0 +1,10 @@ +#include + +int main(void) { + int a = 17, b = 25; + + printf("a: %d @ %p\n", a, &a); + printf("b: %d @ %p\n", b, &b); + + return 0; +}