conteúdo da aula 4

This commit is contained in:
Blau Araujo 2025-03-20 15:05:11 -03:00
parent 5aafa298ee
commit 09d21771ce
17 changed files with 357 additions and 318 deletions

View file

@ -1,229 +0,0 @@
#+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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

View file

@ -1,67 +0,0 @@
#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

@ -1,12 +0,0 @@
#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

@ -1,10 +0,0 @@
#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;
}

View file

@ -0,0 +1,181 @@
#+title: Curso Básico da Linguagem C
#+subtitle: Aula 4: Variáveis e ponteiros
#+author: Blau Araujo
#+startup: show2levels
#+options: toc:3
* Variáveis e ponteiros
- *Variáveis* são elementos da linguagem que representam dados na memória.
- *Ponteiros* são variáveis que representam endereços de dados na memória.
** Declaração e definição
Variáveis precisam ser declaradas e definidas antes de serem utilizadas.
Exemplo (=var1.c=):
#+begin_src c
#include <stdio.h>
int main(void) {
int a = 10; /* Variável declarada e inicializada (definida). */
int b; /* Variável declarada, mas não inicializada! */
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("a + b = %d\n", a + b);
return 0;
}
#+end_src
Compilando e executando:
#+begin_example
:~$ gcc -o var1 var1.c
:~$ ./var1
a = 10
b = 361680176
a + b = 361680186
#+end_example
Observe:
- A compilação não emitiu avisos e nem terminou com erro.
- A variável =b= (não inicializada) avaliou um valor qualquer.
*** Por que isso aconteceu?
Ao ser declarada, a variável recebeu um endereço e um tipo (=int=). Como não
foi inicializada, seu valor será o que quer que esteja nos 4 bytes da
memória a partir do endereço designado para =b=.
Por exemplo:
#+begin_example
a -> 0x7ffd45c33fdc: 0a 00 00 00 (10)
b -> 0x7ffd45c33fd8: 3a cd 8e 15 (361680176) <-- LIXO!
#+end_example
*** Como evitar esse erro?
A primeira (e mais óbvia) opção é não se esquecer de atribuir um valor
à variável antes de usá-la, mas a compilação poderia ter sido feita com
as opções de avisos:
- =-Wall=: Habilita todos os avisos sobre problemas evitáveis no código.
- =-Wextra=: Habilita avisos adicionais.
- =-Werror=: Trata todos os avisos como erros fatais.
Exemplo:
#+begin_example
:~$ gcc -o var1 var1.c -Wall -Wextra -Werror
var1.c: In function main:
var1.c:10:5: error: b is used uninitialized [-Werror=uninitialized]
10 | printf("b = %d\n", b);
| ^~~~~~~~~~~~~~~~~~~~~
var1.c:7:9: note: b was declared here
7 | int b; /* Variável declarada, mas não inicializada! */
| ^
cc1: all warnings being treated as errors
#+end_example
** Atributos das variáveis
Toda variável tem como atributos...
- Um nome (identificador);
- Um valor associado;
- O endereço do valor associado.
O nome da variável expressa o seu valor:
#+begin_src c
int a = 21;
int b = a; // O valor de 'b' é o valor de 'a' (21).
int c = a + b; // O valor de 'c' é 21+21 (42).
#+end_src
Para que uma variável expresse o endereço do valor associado, nós utilizamos
o operador unário =&= antes de seu nome:
#+begin_src c
int a = 123;
printf("Valor de a : %d", a);
printf("Endereço de a: %p", &a); // Imprime o endereço do dado em 'a'.
#+end_src
O tamanho do tipo da variável é expresso com o operador =sizeof(VAR)=:
#+begin_src c
int a = 10;
int b = sizeof(a); // O valor de 'b' será 4 (bytes).
#+end_src
#+begin_quote
O tipo do valor expresso por =sizeof= é =size_t=, um apelido para =unsigned long=.
#+end_quote
** Escopo de variáveis
- Toda variável declarada numa função só é visível na própria função (escopo /local/).
- Toda variável declarada fora de funções é visível em todo o programa (escopo global).
Exemplo (=var3.c=):
#+begin_src c
#include <stdio.h>
int c = 17; // Variável global.
// As variáveis 'num' e 'b' são locais à função 'soma10'.
int soma10(int num) {
int b = 10;
return b + num;
}
int main(void) {
int a = 25; // A variável 'a' é local à função 'main'.
// Isso causará um erro na compilação!
printf("a = %d\nb = %d\nc = %d\nnum = %d\n", a, b, c, num);
return 0;
}
#+end_src
** Ponteiros
Antes de qualquer coisa, os ponteiros são simplesmente variáveis, embora
possuam algumas particularidades:
- São declarados com o operador de derreferência (ou indireção) =*NOME=.
- Recebem endereços como valores.
- Seus tipos são suas unidades aritméticas.
- Seus nomes expressam os endereços associados.
- Quando derreferenciados (=*NOME=), expressam os valores nos endereços.
*** Declaração do ponteiro
#+begin_src c
int a = 25;
int *pa = &a; // O valor de 'pa' será o endereço de 'a'.
int b = *pa; // O valor de 'b' será o valor no endereço em 'pa'.
#+end_src
** Perguntas da aula
*** A cada execução do programa, os endereços das variáveis mudam?
Sim, porque o /espaço de endereços/ é designado pelo sistema operacional
assim que o processo para executar o programa é criado.
*** O ponto da declaração de uma variável global faz diferença?
Sim, toda variável tem que ser declarada antes do ponto no código
em que é utilizada.

BIN
aulas/04-variaveis/a.out Executable file

Binary file not shown.

View file

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

BIN
aulas/04-variaveis/var1 Executable file

Binary file not shown.

14
aulas/04-variaveis/var1.c Normal file
View file

@ -0,0 +1,14 @@
#include <stdio.h>
int main(void) {
int a = 10; /* Variável declarada e inicializada (definida). */
int b; /* Variável declarada, mas não inicializada! */
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("a + b = %d\n", a + b);
return 0;
}

BIN
aulas/04-variaveis/var2 Executable file

Binary file not shown.

15
aulas/04-variaveis/var2.c Normal file
View file

@ -0,0 +1,15 @@
#include <stdio.h>
int main(void) {
int a = 17;
int b = 25;
puts("Var Address Size Value");
printf("a -> %p %4zu %5d\n", &a, sizeof(a), a);
printf("b -> %p %4zu %5d\n", &b, sizeof(b), b);
return 0;
}

16
aulas/04-variaveis/var3.c Normal file
View file

@ -0,0 +1,16 @@
#include <stdio.h>
int c = 17;
int soma10(int num) {
int b = 10;
return b + num;
}
int main(void) {
int a = 25;
printf("a = %d\nb = %d\nc = %d\nnum = %d\n", a, b, c, num);
return 0;
}

BIN
aulas/04-variaveis/var4 Executable file

Binary file not shown.

16
aulas/04-variaveis/var4.c Normal file
View file

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

98
exercicios/04/README.org Normal file
View file

@ -0,0 +1,98 @@
#+title: Curso Básico da Linguagem C
#+subtitle: Exercícios
#+author: Blau Araujo
#+startup: show2levels
#+options: toc:3
* Exercícios da aula 4: Variáveis e ponteiros
- [[../aulas/04-variaveis/README.org][Anotações da aula]]
- [[https://youtu.be/i7RKtMgSSrM][Vídeo]]
** 1. Pesquise e responda
- Existe uma forma para alterar uma variável em uma função a partir de
outra função: como fazer isso?
- Por que as variáveis de uma função, em princípio, são locais à própria
função?
- Se o valor associado a um ponteiro é um endereço, o que termos com a
avaliação de =&NOME_DO_PONTEIRO=?
** 2. Analise o código, pesquise e responda
Este é mais um "Olá, mundo":
#+begin_src c
#include <stdio.h>
char *msg = "Salve, simpatia!";
int main(void) {
puts(msg);
return 0;
}
#+end_src
Se ponteiros recebem endereços como valores, por que eu fiz a atribuição
de uma string e o meu programa funcionou?
** 3. Compile, analise e demonstre
#+begin_src c
#include <stdio.h>
int main(void) {
int a = 0;
int b = 875569217;
int c = 1280655661;
int d = 1129071171;
char *p = (char *)&d;
int i = 0;
while (*(p + i) != '\0') {
putchar(*(p + i));
i++;
}
putchar('\n');
// printf("%p\n%p\n%p\n%p\n", &d, &c, &b, &a);
return 0;
}
#+end_src
- Como o código funciona?
- O que estou tentando imprimir?
- Com o =printf= comentado, eu consigo imprimir o que eu quero?
- Se eu tirar o comentário, eu tenho a impressão que eu quero?
- Por que acontece essa diferença de comportamento?
- Como imprimir corretamente apenas o que eu quero?
** 4. Pesquise e responda
- Para que serve e como usar a função =putchar=?
- Quando e por que utilizar =putchar('\n')= em vez de =puts("")=?
- Como funciona a estrutura de repetição =while=?
- Para que servem os especificadores de formato =%zu= e =%p=?
** 5. Desafio: quadrado de um número
Crie um programa que peça a digitação de um número inteiro ao usuário
e imprima o quadrado desse número.
*Requisitos:*
- O programa deve testar se o número digitado é um inteiro válido.
- Se não for, o programa deve terminar com uma mensagem enviada para
a saída padrão de erros (=stderr=) e estado de término =1=.
*Dicas:*
- Utilize a função =scanf= para ler o número digitado.
- Existe uma variante do =printf= que possibilita imprimir na saída de erros
em vez da saída padrão.
- Existe um cabeçalho que define constante com os limites dos tipos inteiros.