anotações da aula 11

This commit is contained in:
Blau Araujo 2025-04-25 09:00:50 -03:00
parent 95ea0a5bd5
commit 699cf4bd1c

483
aulas/11-scanf/README.org Normal file
View file

@ -0,0 +1,483 @@
#+title: Curso Básico da Linguagem C
#+subtitle: Aula 11: Leitura da entrada padrão com 'scanf'
#+author: Blau Araujo
#+startup: show2levels
#+options: toc:3
* Aula 11: Leitura da entrada padrão com 'scanf'
[[https://youtu.be/MZiI95b2gdY][Vídeo desta aula]]
A entrada padrão (/stream/ =stdin= ou descritor de arquivos =0=) pode estar ligada...
- A um terminal para leitura interativa (digitação);
- A um arquivo via redirecionamento de leitura;
- À saída de um processo via pipe ou arquivo FIFO;
- A um arquivo de forma programática (ex: funções =freopen=, =dup2=).
Para verificar se o fluxo =stdin= está ligado a um terminal, nós podemos
utilizar a função =isatty= (=unistd.h=):
#+begin_src c
int isatty(int fd);
#+end_src
Onde =int fd= é o descritor de arquivos testado. O retorno será o inteiro =1=,
se =fd= estiver ligado a um terminal, ou =0=, se estiver ligado a um pipe ou
redirecionado para um arquivo.
Exemplo (=test-tty.c=):
#+begin_src c
#include <stdio.h>
#include <unistd.h>
int main(void) {
if (isatty(0)) {
puts("STDIN ligada ao terminal.");
} else {
puts("STDIN ligada a um arquivo ou pipe.");
}
return 0;
}
#+end_src
Compilação e testes:
#+begin_example
:~$ gcc -Wall test-tty.c
:~$ ./a.out
STDIN ligada ao terminal.
:~$ ./a.out < /dev/null
STDIN ligada a um arquivo ou pipe.
:~$ : | ./a.out
STDIN ligada a um arquivo ou pipe.
#+end_example
#+begin_quote
O comando interno do shell =:= é o comando nulo, que não faz nada e sempre termina
com sucesso.
#+end_quote
Para associar programaticamente a entrada padrão a um arquivo (de nome recebido
como argumento, por exemplo), nós podemos utilizar a função =freopen= para abrir
um arquivo para leitura através de um fluxo (/stream/) previamente aberto: no caso
o stream =stdin=...
#+begin_src c
FILE *freopen(const char *caminho, const char *modo, FILE *stream);
#+end_src
Onde:
- =caminho=: string do nome do arquivo;
- =modo=: string de modo de abertura (="r"=, ="w"=, ="rw"=);
- =stream=: um fluxo de dados previamente aberto.
Retorna um ponteiro para o tipo =FILE= ou =NULL=, em caso de erro.
#+begin_quote
O propósito original desta função é alterar o arquivo associado a um dos
fluxos padrão (=stdin=, =stdout= ou =stderr=).
#+end_quote
Exemplo (=redir-stdin.c=):
#+begin_src c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
// Diretório 'fd' do processo de 'a.out'...
puts("Antes do redirecionamento...");
system("ls -l /proc/$(pidof a.out)/fd");
// Nome do arquivo...
char *filename;
if (argc > 1) {
// Primeiro argumento...
filename = argv[1];
} else {
// Dispositivo nulo...
filename = "/dev/null";
}
// Redirecionamento...
FILE* stream = freopen(filename, "r", stdin);
if (!stream) {
perror("freopen");
return EXIT_FAILURE;
}
// Diretório 'fd' do processo de 'a.out'...
puts("Depois do redirecionamento...");
system("ls -l /proc/$(pidof a.out)/fd");
return EXIT_SUCCESS;
}
#+end_src
Compilação...
#+begin_example
:~$ gcc -Wall redir-stdin.c
#+end_example
Executando sem argumentos (redireciona para =/dev/null=):
#+begin_example
:~$ ./a.out
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /dev/null
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
#+end_example
Executando com argumentos (nome do arquivo no primeiro argumento):
#+begin_example
:~$ ./a.out /etc/shells
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /etc/shells
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
#+end_example
No caso de um erro de abertura (ex: arquivo "banana" não existe):
#+begin_example
:~$ ./a.out banana
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:21 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 2 -> /dev/pts/0
freopen: No such file or directory
#+end_example
** Leitura interativa da entrada padrão
Seja o conteúdo de um arquivo ou uma entrada digitada no terminal, a leitura
da entrada padrão pode ser feita com várias formas e com diversas funções da
biblioteca padrão:
- Leitura de caracteres: =getchar=, =getc(stdin)= e =fgetc(stdin)=;
- Leitura de linhas (até =\n=): =fgets(buf, size, stdin)=, =getline(&buf, &size, stdin)=;
- Leitura formatada de linhas: =scanf=, =fscanf(stdin, ...)=;
Mas, como a leitura das linhas de um arquivo é mais controlada (o conteúdo
é previamente conhecido), nós vamos concentrar nossa atenção na leitura de
linhas digitadas no terminal por um usuário (leitura interativa). Para isso,
nós estudaremos três funções (nesta e nas próximas aulas):
- Leitura formatada de linhas: =scanf=;
- Leitura de linhas: =fgets=;
- Leitura de caracteres: chamada de sistema =read=.
** Leitura formatada de linhas com 'scanf'
A função =scanf= lê os bytes de uma linha enquanto encontra casamentos com os
especificadores definidos em uma string de formato (como no =printf=). A parte
da linha que encontrar casamento é passada para um ou mais endereços de
variáveis (ou endereços em ponteiros) e tudo que vier depois permanece no
buffer de entrada do processo.
Outros pontos importantes:
- Cada especificador de formato é casado com apenas uma palavra da linha
lida (palavras são cadeias de caracteres delimitadas por espaços,
tabulações e quebras de linha).
- Retorna o número de casamentos com as especificações de formatos que
foram encontrados e atribuídos (o que pode ser menos do que a quantidade
de especificadores fornecidos).
- Em caso de erro, retorna zero ou =EOF= (com vários significados possíveis).
- Altera a variável =errno= (=errno.h=), o que possibilita obter descrições de
erros, por exemplo, com as funções =perror= (=stdio.h=) ou =strerror= (=string.h=).
- No caso da formatação como string, o caractere nulo (='\0'=) é inserido
após o último byte do casamento encontrado.
- Tudo que delimita a quantidade de bytes lidos é a forma como a string
de formato é construída.
- Uma string de formato mal construída pode levar a vulnerabilidades de
memória.
A função =scanf= é considerada insegura por muitos programadores, mas a
verdade é que ela é reconhecidamente difícil de ser utilizada corretamente.
De =man 3 scanf=:
#+begin_quote
It is very difficult to use these functions correctly, and it is preferable
to read entire lines with fgets(3) or getline(3) and parse them later with
sscanf(3) or more specialized functions such as strtol(3).
#+end_quote
Ou seja, o próprio manual diz que é muito difícil utilizar corretamente
as funções da família do =scanf= e pode ser mais fácil ler linhas inteiras
com funções como =fgets= e =getline= e fazer os processamentos necessários
com a função =sscanf= ou outras funções mais especializadas em conversões
de cadeias de caracteres em valores numéricos.
*** Para reduzir as chances de erro
A maior parte dos problemas com =scanf= decorre da conversão de strings.
por isso, é sempre bom atentar para alguns detalhes, como:
- Sempre validar o retorno de =scanf= e fazer o tratamento dos erros.
- Na leitura de uma linha inteira como string (por exemplo, uma linha
que contenha apenas uma palavra), sempre limitar a quantidade de
caracteres lidos no especificador =%s= (ex: =%9s= para ler até 9 bytes).
- Na leitura de caracteres únicos, o especificador =%c= deve ser precedido
de um espaço.
*** Exemplos
**** Leitura de duas strings
#+begin_src c
char str1[10];
char str2[10];
// "%Ns" não deve incluir '\0'!
scanf("%9s", str1);
scanf("%9s", str2);
#+end_src
**** Separando a entrada digitada em campos
#+begin_src c
char str1[10];
char str2[10];
scanf("%9s %9s", str1, str2);
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
#+end_src
**** Lendo caracteres
Neste caso, apenas o primeiro caractere será casado, seja ele qual for.
#+begin_src c
printf("Confirma (s/n)? ");
char reply;
scanf("%c", &reply);
#+end_src
Mas, se o usuário digitar espaços antes dos caracteres esperados,
o primeiro espaço é que será consumido. Para evitar isso...
#+begin_src c
scanf(" %c", &reply);
#+end_src
**** Lendo vários caracteres
#+begin_src c
char a, b, c;
scanf(" %c %c %c", &a, &b, &c);
printf("A:%c B:%c C:%c\n", a, b, c);
#+end_src
**** Lendo e formatando números
Inteiros:
#+begin_src c
printf("Digite um inteiro: ");
int num;
scanf("%d", &num);
#+end_src
Ponto flutuante:
#+begin_src c
printf("Digite um float: ");
float f;
int c = scanf("%f", &f);
#+end_src
*** Descarregando o buffer de entrada
A função =scanf= não consome nada que não case com o formato especificado,
o que inclui a quebra de linha. Todo o restante da linha permanece no buffer
de entrada e, na ausência de uma função da biblioteca padrão para descarregá-lo,
nós podemos criar uma função como esta:
#+begin_src c
void stdin_flush(void) {
char ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
#+end_src
Ela consumiria todos os caracteres restantes no buffer de entrada enquanto
o caractere atribuído a =ch= fosse diferente da quebra de linha (=\n=) ou de =-1=,
que é o valor retornado por =getchar= quando tenta ler caracteres após o fim
de um arquivo (constante simbólica =EOF=).
Isso é particularmente necessário quando houver leituras consecutivas da
entrada padrão com =scanf=...
#+begin_src c
int a;
scanf("%d", &b);
stdin_flush();
int b;
scanf("%d", &b);
stdin_flush();
/* ... */
#+end_src
*** Inicialização de valores numéricos
Nos exemplos anteriores, nós temos um problema que precisa ser considerado:
a leitura pode falhar e, nesse caso, nada será escrito nos endereços das
variáveis passados para =scanf=. Como as variáveis não foram inicializadas,
seu conteúdo será /lixo de memória/. Portanto, é importante que essas variáveis
sejam devidamente inicializadas com algum valor, por exemplo:
#+begin_src c
int a = 0;
scanf("%d", &b);
stdin_flush();
int b = 0;
scanf("%d", &b);
stdin_flush();
/* ... */
#+end_src
Este problema é relevante com variáveis que recebem valores numéricos (=char=,
=int=, =float=, =double=, etc), porque, em geral, elas são utilizadas em cálculos
e até o lixo de memória pode ser tratado como número válido, levando a
resultados enganosos.
*** Buffer overflow
A leitura de strings com =scanf= é bastante suscetível a causar vulnerabilidades
graves de memória, como o /buffer overflow/, que é quando a quantidade de dados
escritos a partir do endereço de um buffer excedem seu limite máximo e continuam
sendo escritos em endereços válidos subsequentes.
#+begin_quote
Quando o programa tentar escrever os bytes excedentes em uma região inválida
da memória, nós temos uma /falha de segmentação/.
#+end_quote
Observe este exemplo:
#+begin_src c
char ref[10] = "123456789"; // Uma string qualquer.
char str[10]; // Buffer para a stringa digitada no terminal.
scanf("%s", str); // Especificador %s sem limite.
printf("str: %s\n", str);
printf("ref: %s\n", ref);
#+end_src
Ao ser compilado e executado, os 10 bytes de =ref= serão escritos na pilha
e, acima deles (em um endereço 10 bytes mais baixo), serão reservados os
10 bytes para o buffer =str=.
Se o usuário digitar =abc=, esta parte da pilha ficará assim:
#+begin_example
+------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13
+------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x00 | lixo | lixo | lixo | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
#+end_example
A impressão no terminal será:
#+begin_example
str: abc
ref: 123456789
#+end_example
Mas, se digitar =abcdefghjij=, este será o resultado:
#+begin_example
buffer overflow
+------+------+------+------+------+------+------+------+------+------+
ref | 0x00 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13
+------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x6A |
+------+------+------+------+------+------+------+------+------+------+
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
#+end_example
E isso vai imprimir:
#+begin_example
str: abcdefghij
ref:
#+end_example
Porque o caractere nulo (=0x00=) foi escrito no décimo primeiro endereço
após o endereço de =str= (endereço =0x0A=), que é o primeiro byte de =ref=.
Esse problema poderia ser evitado especificando o limite do tamanho da
string:
#+begin_src c
char ref[10] = "123456789"; // Uma string qualquer.
char str[10]; // Buffer para a stringa digitada no terminal.
scanf("%9s", str); // Limite de 9 bytes para a string.
printf("str: %s\n", str);
printf("ref: %s\n", ref);
#+end_src
Executando e digitando =abcdefghij=, os dados na pilha seriam...
#+begin_example
+------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x0A 0x0B 0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13
+------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
#+end_example
E nós veríamos isso no terminal:
#+begin_example
str: abcdefghi <- O 'j' não foi consumido...
ref: 123456789
#+end_example