mirror of
https://gitlab.com/blau_araujo/cblc.git
synced 2025-05-09 18:16:37 -03:00
anotações da aula 11
This commit is contained in:
parent
95ea0a5bd5
commit
699cf4bd1c
1 changed files with 483 additions and 0 deletions
483
aulas/11-scanf/README.org
Normal file
483
aulas/11-scanf/README.org
Normal 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
|
||||
|
||||
|
Loading…
Add table
Reference in a new issue