mirror of
https://gitlab.com/blau_araujo/cblc.git
synced 2025-05-09 18:16:37 -03:00
conteúdo da aula 8
This commit is contained in:
parent
3d90f7fad1
commit
957cc38a87
14 changed files with 982 additions and 0 deletions
|
@ -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)
|
||||
|
|
421
aulas/08-processos/README.org
Normal file
421
aulas/08-processos/README.org
Normal 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
BIN
aulas/08-processos/a.out
Executable file
Binary file not shown.
47
aulas/08-processos/fork-exec.c
Normal file
47
aulas/08-processos/fork-exec.c
Normal 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;
|
||||
}
|
99
aulas/08-processos/mmap/#main.c#
Normal file
99
aulas/08-processos/mmap/#main.c#
Normal 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
BIN
aulas/08-processos/mmap/a.out
Executable file
Binary file not shown.
96
aulas/08-processos/mmap/main.c
Normal file
96
aulas/08-processos/mmap/main.c
Normal 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;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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);
|
||||
}
|
229
aulas/08-processos/old/README.org
Normal file
229
aulas/08-processos/old/README.org
Normal 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
BIN
aulas/08-processos/old/a.out
Executable file
Binary file not shown.
BIN
aulas/08-processos/old/jogo-taz.png
Normal file
BIN
aulas/08-processos/old/jogo-taz.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 357 KiB |
BIN
aulas/08-processos/old/mem-layout.png
Normal file
BIN
aulas/08-processos/old/mem-layout.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 101 KiB |
67
aulas/08-processos/old/memlo.c
Normal file
67
aulas/08-processos/old/memlo.c
Normal 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;
|
||||
}
|
12
aulas/08-processos/old/var-global.c
Normal file
12
aulas/08-processos/old/var-global.c
Normal 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;
|
||||
}
|
10
aulas/08-processos/old/var-local.c
Normal file
10
aulas/08-processos/old/var-local.c
Normal 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;
|
||||
}
|
Loading…
Add table
Reference in a new issue