conteúdo da aula 8

This commit is contained in:
Blau Araujo 2025-04-01 09:41:17 -03:00
parent 3d90f7fad1
commit 957cc38a87
14 changed files with 982 additions and 0 deletions

View file

@ -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)

View file

@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
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.

BIN
aulas/08-processos/a.out Executable file

Binary file not shown.

View file

@ -0,0 +1,47 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
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;
}

View file

@ -0,0 +1,99 @@
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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);
}

BIN
aulas/08-processos/mmap/a.out Executable file

Binary file not shown.

View file

@ -0,0 +1,96 @@
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
/*
* 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);
}

View file

@ -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 <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#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

BIN
aulas/08-processos/old/a.out Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -0,0 +1,67 @@
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#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;
}

View file

@ -0,0 +1,12 @@
#include <stdio.h>
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;
}

View file

@ -0,0 +1,10 @@
#include <stdio.h>
int main(void) {
int a = 17, b = 25;
printf("a: %d @ %p\n", a, &a);
printf("b: %d @ %p\n", b, &b);
return 0;
}