From 8e0a426b3232c277acf9341b6995fc7ac973b47a Mon Sep 17 00:00:00 2001 From: Blau Araujo Date: Wed, 18 Jun 2025 09:55:56 -0300 Subject: [PATCH] =?UTF-8?q?corre=C3=A7=C3=A3o=20dos=20links=20dos=20exempl?= =?UTF-8?q?os=20da=20aula=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- curso/aula-08.org | 2508 +++++++++++++++++++++++---------------------- 1 file changed, 1260 insertions(+), 1248 deletions(-) diff --git a/curso/aula-08.org b/curso/aula-08.org index 9ff5b4b..45d91de 100644 --- a/curso/aula-08.org +++ b/curso/aula-08.org @@ -1,4 +1,4 @@ -#+title: 8 -- Fluxos de dados +#+title: 8 -- Conversão de strings numéricas para inteiros #+author: Blau Araujo #+email: cursos@blauaraujo.com @@ -6,1345 +6,1357 @@ * Objetivos -- Compreender o conceito de fluxos de dados. -- Entender a relação entre descritores de arquivos e /streams/. -- Demonstrar como os mecanismos de pipe e redirecionamento são implementados em baixo nível. -- Introduzir a criação de programas em baixo nível que recebem dados interativamente. -- Aprender a diferenciar a origem de um fluxo de entrada de dados. +- Diferenciar as expressões numéricas em texto dos números que representam. +- Conhecer a relação entre tipos numéricos e tamanhos de dados. +- Criar sub-rotinas reaproveitáveis para conversões de caracteres para números. -* Tabela de descritores de arquivos +* Todos os dados externos são recebidos como caracteres -Um /fluxo de dados/ é a abstração, em software, de todas as possíveis combinações -de trocas de dados entre arquivos e processos no espaço de usuário, como a -leitura dos dados digitados no teclado do terminal, a impressão (ou exibição) -de mensagens no /display/ do terminal o acesso a um arquivo para escrita e/ou -leitura de dados e até a mediação da troca de dados entre dois processos. +Se consideramos como /dados externos/: -No nível do sistema operacional, esses fluxos são estabelecidos e controlados -por diversas estruturas (/structs/), associadas individualmente a cada processo, -chamadas de /tabelas de descritores de arquivos/, onde cada entrada é um inteiro -sem sinal chamado de /descritor de arquivo/ (ou FD, de /file descriptor/). +- Argumentos de linha de comando; +- Variáveis exportadas para o ambiente do processo; +- Dados digitados pelo usuário; +- Dados lidos por redirecionamentos e pipes; +- Dados lidos de arquivos em geral... -O diagrama abaixo representa, conceitualmente, como o kernel do Linux organiza -os descritores de arquivos e suas estruturas internas para cada processo: +Todos eles têm em comum o fato de serem recebidos nos nossos programas +como cadeias de caracteres, mesmo que representem números. Consequentemente, +para utilizá-los em operações numéricas, será necessário convertê-los nos +valores que representam. -#+begin_example -TABELA DE DESCRIOTES - DE ARQUIVOS ┌─────────────┐ ┌──────────────────┐ - ┌──────────────┐ │ ├───────┤ FILE OPETRATIONS │ - │ PROCESSO │ ┌──┤ STRUCT FILE ├────┐ └──────────────────┘ - ├──────────────┤ │ │ ├──┐ │ ┌──────────────────┐ - │ FD0 (STDIN) ├──┘ └─────────────┘ │ └──┤ INODE │ - ├──────────────┤ │ └──────────────────┘ - │ FD1 (STDOUT) ├── ··· │ ┌──────────────────┐ - ├──────────────┤ └────┤ DIR ENTRY │ - │ FD2 (STDERR) ├── ··· └──────────────────┘ - ├──────────────┤ ··· - │ ··· ├── ··· - └──────────────┘ -#+end_example +** Entendendo o problema -Nele, nós podemos ver que cada entrada da tabela (um descritor de arquivo) está -associada a uma estrutura de dados (/struct file/), gerenciada pelo kernel, onde -são encontradas diversas informações sobre um arquivo e as operações que serão -realizadas com ele. +Para entendermos melhor o problema, vamos analisar o que acontece no programa +abaixo (=exemplo.c=) com o GDB. -** Uma nota sobre tipos de arquivos +Arquivo: [[exemplos/08/exemplo.c][exemplo.c]] -Sistemas parecidos com o UNIX, como o GNU/Linux, trabalham com 7 tipos -de arquivos: +#+begin_src c :tangle exemplos/08/exemplo.c +#include -- Diretórios :: +int main(void) { - Arquivos que indexam a localização de outros arquivos. - -- Arquivos comuns :: - - Como os arquivos dos nossos textos, programas e imagens. - -- Ligações simbólicas :: - - Arquivos que representam outros arquivos. - -- Dispositivos caractere :: - - Abstração da comunicação com dispositivos de hardware sem o acúmulo de dados. - -- Dispositivos bloco :: - - Abstração da comunicação com dispositivos de hardware com acúmulo de dados. - -- Pipes/FIFOs :: - - Arquivos que interfaceiam a troca de dados entre processos. - -- Sockets :: - - Arquivos de interfaceiam o acesso a serviços. - -Então, quando dissermos algo como: /"dispositivo de terminal"/, ou /"ligações -simbólicas"/, nós estaremos nos referindo a diferentes tipos de arquivos. - -** Descritores de arquivos padrão - -No sistema de arquivos, já no espaço de usuário, os descritores de arquivos -são disponibilizados aos processos como /ligações simbólicas/, e todo processo -é iniciado com acesso a três deles -- os /descritores de arquivos padrão/: - -- *Descritor de arquivo =0=:* para leitura de fluxos de dados; -- *Descritor de arquivo =1=:* para escrita de dados; -- *Descritor de arquivo =2=:* para escrita de dados relacionados a erros. - -Esses descritores, assim como todos os outros criados ao longo da execução -do processo, podem ser encontrados no diretório virtual =/proc//fd= e, -em princípio, eles estão ligados a um /dispositivo de terminal/. Os descritores -de arquivos padrão também são expostos no sistema de arquivos pelas ligações -simbólicas =stdin=, =stout= e =stderr=, no diretório =/dev= (de /device/): - -#+begin_example -:~$ ls -l /dev/std* -lrwxrwxrwx 1 root root 15 mai 28 11:30 /dev/stderr -> /proc/self/fd/2 -lrwxrwxrwx 1 root root 15 mai 28 11:30 /dev/stdin -> /proc/self/fd/0 -lrwxrwxrwx 1 root root 15 mai 28 11:30 /dev/stdout -> /proc/self/fd/1 -#+end_example - -Isso mostra claramente a relação entre os nomes simbólicos e os descritores -de arquivos que representam: - -- =stdin= → Descritor de arquivo =0= -- =stdout= → Descritor de arquivo =1= -- =stderr= → Descritor de arquivo =2= - -Ainda na listagem anterior, note que as ligações apontam para os descritores -de arquivos padrão de um processo chamado =self= (/"eu mesmo"/, em inglês), que -é um /link mágico/ que expande o PID do processo que acessa o diretório =fd= -através das ligações =/dev/std*=. - -Por exemplo: - -#+begin_example -:~$ grep -E '^(Name:|Pid:)' /proc/self/status -Name: grep -Pid: 262881 -#+end_example - -Isso fez com que o arquivo =status=, no diretório do processo do próprio =grep=, -fosse filtrado para imprimir apenas as linhas iniciadas com =Nome:= e =Pid:=. Nós -também podemos usar =self= para expandir a lista de arquivos no diretório =fd= -do processo do shell em execução em um terminal: - -#+begin_example -:~$ printf '%s\n' /proc/self/fd/* -/proc/self/fd/0 -/proc/self/fd/1 -/proc/self/fd/2 -/proc/self/fd/255 -/proc/self/fd/3 -#+end_example - -#+begin_quote -No Bash, =printf= é um comando interno. -#+end_quote - -Além dos descritores padrão (=0=, =1= e =2=), nós temos os descritores =255=, criado -pelo shell para uso interno, e =3=, que é criado pelo kernel para resolver o -caminho expandido por =self=, como podemos demonstrar listando os descritores -do processo do =ls= utilizado para listar seu próprio diretório =fd=: - -#+begin_example -:~$ ls -l /proc/self/fd -total 0 -lr-x------ 1 blau blau 64 mai 30 07:16 3 -> /proc/263784/fd -lrwx------ 1 blau blau 64 mai 30 07:16 0 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 07:16 1 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 07:16 2 -> /dev/pts/0 -#+end_example - -Sabendo o número do PID, nós podemos listar o conteúdo do diretório =fd= -associado ao processo sem usar =fd=. Por exemplo, nós podemos descobrir o PID -do processo do shell em execução no terminal com: - -#+begin_example -:~$ echo $$ -252271 -#+end_example - -Em seguida, nós utilizamos o PID encontrado: - -#+begin_example -:~$ ls -l /proc/252271/fd -total 0 -lrwx------ 1 blau blau 64 mai 30 05:56 0 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 05:56 1 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 05:56 2 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 05:56 255 -> /dev/pts/0 -#+end_example - -#+begin_quote -A expansão do PID do shell também poderia ser feita diretamente na linha de -comando com: =ls -l /proc/$$/fd=. -#+end_quote - -Como podemos ver, as ligações simbólicas =0=, =1= e =2= estão presentes e apontam -para o dispositivo de terminal =/dev/pts/0=. Além deles, nós também podemos -ver que o shell criou um quarto descritor de arquivo (=255=) para seu uso -interno, mas o descritor =3= não existe, porque nós não usamos =self=. - -** Abstração em alto nível (/stream/) - -Acima da interface de baixo nível, a biblioteca padrão da linguagem C (=glibc=) -introduz uma camada de abstração bastante conveniente por meio de /streams/ -(de /fluxos/, em inglês), representadas por ponteiros do tipo =FILE *= para uma -estrutura de controle que, entre outras coisas, encapsula os descritores de -arquivos. Para entender a diferença entre descritores de arquivos e /streams/, -imagine um arquivo como um livro numa biblioteca: - -- *Descritor de arquivo:* é como ter apenas o número de catálogo do livro no - sistema da biblioteca -- você sabe onde o livro está, mas precisa ir até - ele e manuseá-lo por conta própria. - -- *Stream:* é como ter um assistente que encontra o livro, traz até você, - lembra em que página você parou, faz anotações enquanto você lê, e ainda - avisa se houver algum problema com o conteúdo. - -A =glibc=, portanto, fornece diversas funções de alto nível utilizando /streams/ -para abertura e fechamento de ligações com arquivos e tarefas de leitura e -escrita, além de recursos como bufferização automática, verificação de erros e -manipulação de arquivos. Os fluxos padrão (=stdin=, =stdout= e =stderr=) também são -inicializados automaticamente como /streams/ assim que o programa é executado, -apontando para os descritores =0=, =1= e =2=, respectivamente. - -* Uso dos fluxos de dados padrão no shell - -O shell utiliza o fluxo padrão de entrada (=stdin=) para ler os comandos -digitados no terminal e os fluxos padrão de saída para imprimir, também no -terminal, as saídas de comandos (=stdout=) e mensagens de erro (=stderr=). Os -programas executados na linha de comandos, por sua vez, herdarão os -descritores de arquivos do shell e também utilizarão os fluxos padrão para -ler e escrever no mesmo terminal: - -#+begin_example -:~$ ls -l /proc/self/fd -total 0 -lr-x------ 1 blau blau 64 mai 30 07:49 3 -> /proc/266285/fd -lrwx------ 1 blau blau 64 mai 30 07:49 0 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 07:49 1 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 07:49 2 -> /dev/pts/0 -:~$ ls -l /proc/$$/fd -total 0 -lrwx------ 1 blau blau 64 mai 30 05:56 0 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 05:56 1 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 05:56 2 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 05:56 255 -> /dev/pts/0 -#+end_example - -Veja que o processo do programa =ls= (primeira listagem) herdou o acesso ao -pseudo terminal =/dev/pts/0= de seu processo pai -- o processo do shell, na -segunda listagem. Contudo, existem dois mecanismos, implementados como -operadores do shell, que possibilitam alterar os arquivos associado aos -descritores de arquivo padrão: /redirecionamentos/ e /pipes/. - -** Mecanismo de redirecionamento - -Com os redirecionamentos, nós podemos indicar os arquivos que substituirão -o terminal como origem e destino de fluxos de dados. Por exemplo, em vez -de imprimir a saída do =ls= no terminal, nós podemos desviar o fluxo em -=stdout= para o arquivo =exemplo.txt= com os operadores =>= ou =>>=. Ambos criam -o arquivo de destino se ele não existir, mas lidam de formas diferentes -com arquivos existentes: - -- *Operador de escrita em =stdout= (=>=):* trunca (apaga) o conteúdo do arquivo - antes de iniciar a escrita. -- *Operador de /append/ em =stdout= (=>>=):* inclui as novas linhas após o conteúdo - existente no arquivo. - -Como o nosso arquivo ainda não existe, tanto faz o operador, portanto... - -#+begin_example -:~$ ls -l /proc/self/fd > exemplo.txt -:~$ -#+end_example - -Nada foi impresso no terminal, mas o arquivo =exemplo.txt= foi criado e a -saída do =ls= foi escrita nele: - -#+begin_example -:~$ cat exemplo.txt -total 0 -lr-x------ 1 blau blau 64 mai 30 08:04 3 -> /proc/267394/fd -lrwx------ 1 blau blau 64 mai 30 08:04 0 -> /dev/pts/0 -l-wx------ 1 blau blau 64 mai 30 08:04 1 -> /home/blau/exemplo.txt -lrwx------ 1 blau blau 64 mai 30 08:04 2 -> /dev/pts/0 -#+end_example - -Observe que o descritor de arquivo =1= (=stdout=) estava ligado ao arquivo -=/home/blau/exemplo.txt= durante a execução do =ls=. O mesmo poderia ser -feito com a saída padrão de erros, mas nós teríamos que informar o número -do descritor de arquivos (=2=) junto ao operador. Por exemplo, a tentativa -de listar um arquivo inexistente causaria um erro e imprimiria uma mensagem -no terminal via =stderr=: - -#+begin_example -:~$ ls banana.txt -ls: não foi possível acessar 'banana.txt': Arquivo ou diretório inexistente -#+end_example - -Esta saída poderia ser redirecionada para o arquivo =exemplo.txt=: - -#+begin_example -:~$ ls -l /proc/self/fd banana.txt 2> exemplo.txt -/proc/self/fd: -total 0 -lr-x------ 1 blau blau 64 mai 30 08:14 3 -> /proc/268233/fd -lrwx------ 1 blau blau 64 mai 30 08:14 0 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 08:14 1 -> /dev/pts/0 -l-wx------ 1 blau blau 64 mai 30 08:14 2 -> /home/blau/exemplo.txt -#+end_example - -Aqui, a listagem em =stdout= foi impressa no terminal e a mensagem de erro -foi escrita no arquivo: - -#+begin_example -:~$ cat exemplo.txt -ls: não foi possível acessar 'banana.txt': Arquivo ou diretório inexistente -#+end_example - -Na listagem impressa, aliás, nós também podemos ver que o descritor de -arquivo =2= foi redirecionado para o arquivo. A entrada padrão também pode -ser redirecionada para a leitura de um arquivo, mesmo que o programa não -o utilize. Isso é feito com o operador de redirecionamento de leitura (=<=): - -#+begin_example -$ ls -l /proc/self/fd < exemplo.txt -total 0 -lr-x------ 1 blau blau 64 mai 30 08:20 3 -> /proc/268655/fd -lr-x------ 1 blau blau 64 mai 30 08:20 0 -> /home/blau/exemplo.txt -lrwx------ 1 blau blau 64 mai 30 08:20 1 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 08:20 2 -> /dev/pts/0 -#+end_example - -Veja que o descritor de arquivo =0= estava ligado a =exemplo.txt= durante -a execução de =ls=. - -** Mecanismo de pipe - -Enquanto o mecanismo de redirecionamento conecta processos a arquivos, -o mecanismo de /pipe/ conecta o fluxo padrão de saída de um processo ao -fluxo padrão de entrada de outro processo através de um arquivo do tipo -/pipe/. - -#+begin_example - -┌────────────┐ STDOUT ┌──────┐ STDIN ┌────────────┐ -│ PROCESSO 1 ├─────────▶│ PIPE ├────────▶│ PROCESSO 2 │ -└────────────┘ └──────┘ └────────────┘ -#+end_example - -No shell, o mecanismo é implementado com o operador de pipe (=|=) e expressa -perfeitamente, na forma de interface com o usuário, todos os princípios da -Filosofia Unix. Programas como =ls= e =cat=, por exemplo, são especializados -em fazer muito bem apenas uma coisa (listar diretórios e concatenar -conteúdos de arquivos), e ambos são capazes de trabalhar juntos através de -um fluxo de texto canalizado pelo mecanismo do /pipe/: - -#+begin_example -:~$ ls -l /proc/self/fd /proc/$(pidof cat)/fd | cat -/proc/270862/fd: -total 0 -lr-x------ 1 blau blau 64 mai 30 08:48 0 -> pipe:[1689012] -lrwx------ 1 blau blau 64 mai 30 08:48 1 -> /dev/pts/0 -lrwx------ 1 blau blau 64 mai 30 08:48 2 -> /dev/pts/0 - -/proc/self/fd: -total 0 -lr-x------ 1 blau blau 64 mai 30 08:48 3 -> /proc/270861/fd -lrwx------ 1 blau blau 64 mai 30 08:48 0 -> /dev/pts/0 -l-wx------ 1 blau blau 64 mai 30 08:48 1 -> pipe:[1689012] -lrwx------ 1 blau blau 64 mai 30 08:48 2 -> /dev/pts/0 -#+end_example - -Neste exemplo, o =ls= foi executado para listar o conteúdo de seu diretório -=fd= e o diretório do processo do =cat= que, por estar encadeado por pipe na -linha do comando, foi executado simultaneamente e seu PID foi obtido com -o utilitário =pidof=. Como resultado, nós temos os dois diretórios, onde -podemos ver que a entrada padrão do processo do =cat= (=FD0=) estava ligada -ao mesmo arquivo pipe conectado à saída padrão do =ls= (=FD1=). - -** Captura da entrada padrão - -Uma terceira forma de manipulação de fluxos de dados,é a possibilidade -de pausar a execução do shell para ler a entrada padrão e armazenar os -dados lidos em uma ou mais variáveis. Isso é feito por meio do comando -=read=, implementado como um comando interno do shell: - -#+begin_example -read [-r] VAR [VARS ...] # Sintaxe POSIX -read [OPÇÕES] [VAR ...] # Sintaxe no Bash -#+end_example - -#+begin_quote -Nas versões POSIX, o =read= lê apenas a entrada padrão, mas o Bash inclui -uma opção para a leitura de qualquer descritor de arquivo (opção =-u=). -#+end_quote - -Com o =read=, é possível receber dados interativamente, ler uma linha de um -arquivo por redirecionamento de leitura ou ler uma linha da saída de outro -comando encadeado por pipe, mas em circunstâncias bem distintas: - -| Critério | Leitura interativa | Redirecionamento | Pipe | -|-------------------+--------------------+------------------+--------------| -| *Origem da entrada* | Terminal | Arquivo | Arquivo pipe | -| *Fim da leitura* | =Enter= | =\n= ou =EOF= | =\n= ou =EOF= | -| *Interrupção* | =Ctrl+D= | Não | Não | -| *Processo* | Shell corrente | Shell corrente | /Subshell/ | -| *Dados capturados* | Disponíveis | Disponíveis | Inexistentes | - -*** Origem da entrada - -Enquanto pipes e redirecionamentos alteram a ligação com o descritor de -arquivo =0= (=stdin=), a leitura interativa utiliza a ligação original com -o terminal associado ao processo corrente do shell. Em scripts, isso pode -ser detectado através de vários testes, como: - -#+begin_example -test -t 0 # Comando 'test' (POSIX e Bash) -[ -t 0 ] # Comando '[' (POSIX e Bash) -[[ -t 0 ]] # Comando composto '[[ ... ]]', do Bash -#+end_example - -Nos três casos, os comandos terminam com /sucesso/ (estado =0=) se o descritor -de arquivo =0= estiver ligado a um terminal. - -*** Fim da leitura - -O comando interno =read= lê apenas uma linha de dados, o que geralmente é -delimitado pelo caractere de nova linha (=\n= na notação ANSI-C ou =0x0a= -na tabela ASCII) ou quando não houver mais caracteres para ler (fim do -arquivo). Contudo, em leituras não interativas, o Bash possibilita a -alteração do caractere delimitador de linha =-d=. - -Numa leitura interativa, onde a linha é digitada pelo usuário no terminal, -o fim da leitura acontece quando o terminal recebe o comando para enviar -os caracteres lidos, o que é feito com a tecla =Enter=. - -*Notas:* - -- Por padrão, o delimitador de fim de linha é descartado. -- O Bash também implementa a possibilidade de finalizar o comando após a - leitura de um dado número de caracteres (opções =-n= e =-N=). - -*** Interrupção da leitura - -No modo interativo, a leitura da entrada padrão pode ser cancelada com a -combinação de teclas =Ctrl+D=, que envia para o terminal o caractere ASCII -=EOT= (fim de transmissão). Já em pipes e redirecionamentos, a leitura não -pode ser cancelada, mas o término dos processos envolvidos pode ser -forçado, por exemplo, com a combinação de teclas =Ctrl+C=, que envia para -eles o sinal =SIGINT=. - -*** Processos envolvidos na leitura - -Como o =read= é um comando interno do shell, sua execução não dá início -a um novo processo. Isso vale para seu uso em comandos simples, mesmo -que a leitura seja feita por um redirecionamento de leitura. Entretanto, -se o =read= estiver encadeado por pipe com outros comandos, haverá o início -de um novo processo do shell para executá-lo (um /subshell/), o que traz -consequências importantes quanto à disponibilidade dos dados capturados. - -*** Disponibilidade dos dados capturados - -A linha lida pelo =read= é separada em campos conforme os caracteres -definidos na variável interna =IFS=. Por padrão, esses caracteres são -o espaço e a tabulação -- o caractere de nova de linha também está -em =IFS=, mas é utilizado como delimitador de linha. O que determina -a quantidade de campos é a quantidade de argumentos passados para o -=read= como nomes de variáveis, por exemplo: - -#+begin_example -read # Toda a linha será atribuída à variável interna 'REPLY' -read var # Toda a linha será atribuída à variável 'var' -read a b c # 'a' recebe o primeiro campo, 'b' o segundo e 'c' o restante da linha -#+end_example - -De qualquer forma, todos os dados capturados serão atribuídos a variáveis -de escopo local: ou seja, disponíveis apenas para o processo do shell em -que o =read= for executado. Sendo assim, quando o =read= é executado em um -pipe, todas as variáveis definidas serão perdidas quando o processo do -subshell em que ele for executado terminar. - -* Uso dos fluxos de dados padrão em programas - -Em baixo nível, os descritores de arquivos padrão são utilizados diretamente -como argumentos de chamadas de sistema para leitura ou escrita no terminal -associado à execução do processo do programa, como as chamadas =read= e =write=, -quando passamos os descritores =0= e =1=, respectivamente, como argumentos. - -Também existem chamadas de sistema que possibilitam a escrita de programas -que redirecionam os fluxos padrão para outros arquivos ou para pipes, como -é o caso da chamada =dup= quando utilizada em conjunto com a chamada =open= (ou -a chamada =pipe=) para trocar o descritor associado ao arquivo aberto por um -dos descritores de arquivos padrão. - -** Demonstrando um redirecionamento interno - -Por exemplo, se eu quiser que a saída do meu programa (=salve-redir.asm=) seja -escrita em um arquivo, e não no terminal, eu posso redirecionar o descritor -de arquivo =1= com a chamada de sistema =dup2= após criar o arquivo de destino. - -#+begin_quote -Obviamente, essa é a demonstração de um redirecionamento para escrita em -=stdout= tal como é feita pelo shell: nós não precisamos redirecionar nada -nos nossos programas para escrever em arquivos. -#+end_quote - -Arquivo: [[exemplos/08/salve-redir.asm][salve-redir.asm]] - -#+begin_src asm :tangle exemplos/08/salve-redir.asm -; Chamadas de sistema... -%define SYS_WRITE 1 -%define SYS_OPEN 2 -%define SYS_CLOSE 3 -%define SYS_DUP2 33 -%define SYS_EXIT 60 - -; Flags da chamada SYS_OPEN... -%define O_WRONLY 0x1 ; apenas para escrita -%define O_CREAT 0x40 ; cria se não existir -%define O_TRUNC 0x200 ; trunca conteúdo existente - -; Permissões do arquivo criado... -%define F_MODE 0o644 - -; Descritores de arquivos... -%define STDOUT 1 - -section .rodata - file db "mensagem.txt", 0 ; arquivo de destino - msg db "Salve, simpatia!", 10 ; Mensagem - len equ $ - msg ; Tamanho da mensagem - -section .text - global _start - -_start: - ; open (ARQUIVO, FLAGS, MODO) - mov rax, SYS_OPEN - mov rdi, file - mov rsi, O_WRONLY | O_CREAT | O_TRUNC - mov rdx, F_MODE - syscall - - mov r12, rax ; salvar descritor de arquivo retornado - - ; dup2(ANTIGO_FD, NOVO_FD) <-- redirecionamento - mov rax, SYS_DUP2 - mov rdi, r12 - mov rsi, STDOUT - syscall - - ; write(FD, STRING, SIZE) - mov rax, SYS_WRITE - mov rdi, STDOUT - mov rsi, msg - mov rdx, len - syscall - - ; close(FD) - mov rax, SYS_CLOSE - mov rdi, r12 - syscall - - ; exit(STATUS) - mov rax, SYS_EXIT - xor rdi, rdi - syscall + int a = 123; + char *b = "123"; + int *c = (int *)b; // Casting do valor no endereço 'b' para inteiro + + printf("Inteiro a: %d\n", a); + printf("String b: %s\n", b); + printf("Inteiro c: %d\n", *c); // Imprime o valor no endereço 'c' + + return 0; +} #+end_src -Montagem e execução: +*Compilação e execução:* #+begin_example -:~$ nasm -f elf64 salve-redir.asm -:~$ ld -o salve-redir salve-redir.o -:~$ ./salve-redir -:~$ cat mensagem.txt -Salve, simpatia! +:~$ gcc -g exemplo.c +:~$ ./a.out +Inteiro a: 123 +String b: 123 +Inteiro c: 3355185 #+end_example -** Demonstrando um pipe interno +Note que o ponteiro =c=, que recebeu endereço dos bytes da string no ponteiro +=b= (="123"=), aponta para um valor inteiro totalmente diferente do que está +representado pelos dígitos da string (o inteiro =3355185=). -O programa abaixo (=salve-pipe.asm=) demonstra o encadeamento por pipe entre -ele mesmo e o utilitário =cat=, também utilizando os descritores de arquivos -padrão, como o shell faria. A ideia é emular o comportamento do comando: +*Abertura e execução com o GDB:* + +Para examinar os conteúdo da memória em hexadecimal, nós vamos marcar a +unção =main= como ponto de parada, executar o programa e avançar a execução +em 3 linhas, para que as variáveis sejam inciadas, o nos levará ao ponto +anterior à chamada da função =printf=: #+begin_example -echo 'Salve, simpatia!' | cat -#+end_example +:~$ gdb a.out +Reading symbols from a.out... +(gdb) break main +Breakpoint 1 at 0x1141: file exemplo.c, line 5. +(gdb) run +Starting program: /home/blau/git/pbn/curso/exemplos/08/a.out +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". -Arquivo: [[exemplos/08/salve-pipe.asm][salve-pipe.asm]] - -#+begin_src asm :tangle exemplos/08/salve-pipe.asm -; ---------------------------------------------------------- -; Arquivo : salve-pipe.asm -; Reproduz: echo 'Salve, simpatia!' | cat -; Montagem: nasm -f elf64 salve-pipe.asm -; Ligação : ld -o salve-pipe salve-pipe.o -; ---------------------------------------------------------- -; Chamadas de sistema... -; ---------------------------------------------------------- -%define SYS_WRITE 1 -%define SYS_CLOSE 3 -%define SYS_PIPE 22 -%define SYS_DUP2 33 -%define SYS_FORK 57 -%define SYS_EXECVE 59 -%define SYS_EXIT 60 -%define SYS_WAIT 61 -; ---------------------------------------------------------- -; Parâmetros de SYS_WAIT... -; ---------------------------------------------------------- -%define W_CHILD -1 ; filho de qualquer PID -%define W_STATUS 0 ; ignorar o estado de término (NULL) -%define W_OPTIONS 0 ; sem opções -; ---------------------------------------------------------- -; Descritores de arquivos padrão... -; ---------------------------------------------------------- -%define STDIN_FD 0 -%define STDOUT_FD 1 -; ---------------------------------------------------------- -; Estados de término... -; ---------------------------------------------------------- -%define EXIT_SUCCESS 0 -%define EXIT_ERROR 1 -; ---------------------------------------------------------- -section .rodata -; ---------------------------------------------------------- - ; Mensagem... - msg db "Salve, simpatia!", 10 - len equ $ - msg - - ; Argumentos de execve... - comm db "/bin/cat", 0 - argv dq comm, 0 - envp dq 0 -; ---------------------------------------------------------- -section .bss -; ---------------------------------------------------------- - read_end resd 1 ; Descritor da ponta de leitura do pipe - write_end resd 1 ; Descritor da ponta de escrita do pipe -; ---------------------------------------------------------- -section .text -; ---------------------------------------------------------- -global _start - -_start: - ; pipe(int pipefd[]) - ; retorna: pipefd[0] => read_end; pipefd[1] => write_end - mov rax, SYS_PIPE - mov rdi, read_end ; Retorno vai avançar em write_end - syscall - -.parada1: ; examinar FDs retornados! - - ; fork() - mov rax, SYS_FORK - syscall - - test rax, rax ; rax = 0 -> processo filho - jz .filho ; pula a execução do código do pai -; ---------------------------------------------------------- -; Código executado apenas no processo pai... -; ---------------------------------------------------------- -.pai: -; ---------------------------------------------------------- - ; dup2(write_end, STDOUT) - mov rax, SYS_DUP2 - mov edi, [write_end] ; FD original: write_end - mov rsi, STDOUT_FD ; novo descritor de escrita - syscall - - ; Fechar write_end e read_end... - call _close_all - -.parada2: ;examinar /proc//fd! - - ; write(STDOUT, msg, len) - mov rax, SYS_WRITE - mov rdi, STDOUT_FD - mov rsi, msg - mov rdx, len - syscall - - ; wait4(-1, NULL, 0) - mov rax, SYS_WAIT - mov rdi, W_CHILD - mov rsi, W_STATUS - mov rdx, W_OPTIONS - - ; exit(0) - mov rax, SYS_EXIT - mov rdi, EXIT_SUCCESS ; sai com sucesso se chegar aqui - syscall -; ---------------------------------------------------------- -; Código executado apenas no processo filho... -; ---------------------------------------------------------- -.filho: -; ---------------------------------------------------------- - ; dup2(read_end, STDIN) - mov rax, SYS_DUP2 - mov edi, [read_end] ; descritor de leitura no pipe - mov rsi, STDIN_FD ; novo descritor de leitura - syscall - - ; Fechar write_end e read_end... - call _close_all - - ; execve("/bin/cat", ["/bin/cat", NULL], [NULL]) - mov rax, SYS_EXECVE - mov rdi, comm - mov rsi, argv - mov rdx, envp - syscall - - ; exit(1) - mov rax, SYS_EXIT - mov rdi, EXIT_ERROR ; sai com erro se execve falhar - syscall -; ---------------------------------------------------------- -; Sub de fechamento dos descritores originais do pipe... -; ---------------------------------------------------------- -_close_all: -; ---------------------------------------------------------- - ; close(read_end) - mov rax, SYS_CLOSE - mov rdi, [read_end] - syscall - ; close(write_end) - mov rax, SYS_CLOSE - mov rdi, [write_end] - syscall - ret -#+end_src - -Montagem e execução: - -#+begin_example -:~$ nasm -g -felf64 salve-pipe.asm -:~$ ld -o salve-pipe salve-pipe.o -:~$ ./salve-pipe -Salve, simpatia! -#+end_example - -Não há como observar o que o programa faz apenas pelo seu resultado, então -vamos usar o =gdb= para analisar seus efeitos em dois pontos-chave (repare que -montamos o programa com a opção =-g=): - -- Após a criação do pipe (=.parada1:=); -- Antes da impressão da mensagem (=.parada2:=). - -Nossos objetivos serão: - -- Observar os descritores de arquivos retornados para as pontas de leitura - e escrita do pipe; -- Observar o diretório =/proc//fd= do processo pai após a criação do pipe; -- Observar o diretório =/proc//fd= do processo pai antes da impressão da mensagem; -- Observar o diretório =/proc//fd= do processo do =cat= antes da impressão da mensagem. - -*Abertura do programa no GDB:* - -#+begin_example -:~$ gdb ./salve-pipe -Reading symbols from ./salve-pipe... +Breakpoint 1, main () at exemplo.c:5 +warning: Source file is more recent than executable. +5 int a = 123; +(gdb) next 3 +9 printf("Inteiro a: %d\n", a); (gdb) #+end_example -*Definição dos pontos de parada e execução:* +O que queremos observar é a diferença entre os valores numéricos dos três +dados na memória, como se todos eles fossem inteiros (formato =w= no GDB): #+begin_example -(gdb) b _start.parada1 -Breakpoint 1 at 0x401011: file salve-pipe.asm, line 64. -(gdb) b _start.parada2 -Breakpoint 2 at 0x401035: file salve-pipe.asm, line 86. -(gdb) r -Starting program: /home/blau/git/pbn/curso/exemplos/08/salve-pipe - -Breakpoint 1, _start.parada1 () at salve-pipe.asm:64 -64 mov rax, SYS_FORK -(gdb) +(gdb) x /1wx &a +0x7fffffffdecc: 0x0000007b +(gdb) x /1wx b +0x555555556004: 0x00333231 +(gdb) x /1wx c +0x555555556004: 0x00333231 #+end_example -Neste ponto, a criação do pipe acabou de ser executada: +Veja que os bytes da string apontada por 'b' são, como esperado, os mesmos +do inteiro apontado por 'c', mas ambos são totalmente diferentes do valor +em hexadecimal do inteiro em =a= (=123=). -#+begin_src asm -mov rax, SYS_PIPE -mov rdi, read_end ; Retorno vai avançar em write_end -syscall -#+end_src +** A ordem dos bytes -Quando a chamada =pipe= é executada, um arquivo do tipo pipe é criado e dois -descritores de arquivos são associados a ele no processo: +Nós também podemos examinar o conteúdo byte a byte desses dados a partir de +seus endereços iniciais: #+begin_example -┌──────────┐ -│ ├─ 0 -│ ├─ 1 -│ PROCESSO ├─ 2 ┌──────┐ -│ │◀─── READ_END ───┤ │ -│ │ │ PIPE │ -│ ├─── WRITE_END ──▶│ │ -└──────────┘ └──────┘ +(gdb) x /4bx &a +0x7fffffffdecc: 0x7b 0x00 0x00 0x00 +(gdb) x /4bx b +0x555555556004: 0x31 0x32 0x33 0x00 +(gdb) x /4bx c +0x555555556004: 0x31 0x32 0x33 0x00 #+end_example -Os números dos descritores de arquivos são dois inteiros (4 bytes cada) que, -no nosso programa, são escritos a partir do endereço representado pelo rótulo -=read_end=. Sendo assim, vamos examinar seus valores: +Isso ajuda a demonstrar que os bytes de inteiros, como os de qualquer outro +tipo diferente de =char=, são escritos em ordem inversa (/little endian/) na +memória. Portanto, quando os bytes da string foram interpretados como os +4 bytes de um inteiro, eles também foram invertidos: #+begin_example -(gdb) x /1gx &read_end -0x403034 : 0x0000000400000003 -(gdb) x /1wx &read_end -0x403034 : 0x00000003 -(gdb) x /1wx &write_end -0x403038 : 0x00000004 +(gdb) x /4bx c +0x555555556004: 0x31 0x32 0x33 0x00 +(gdb) x /1wx c +0x555555556004: 0x00333231 #+end_example -Isso mostra que os descritores de arquivos do pipe são =3= (ponta de leitura) -e =4= (ponta de escrita). Sabendo o PID do programa, nós podemos listar o -conteúdo de seu diretório =fd= em outro terminal: +O tipo =char= é composto por apenas um byte e a ordem /little endian/ só afeta +a disposição de bytes: é por isso que somente os dados de tipos com dois ou +mais bytes são invertidos. Além disso, na montagem dos dados na memória, +o que importa é o tamanho de cada dado, não o que eles formam. Então, como +strings são formadas por cadeias de caracteres terminadas com o caractere +nulo (='\0'=), a ordem de seus bytes não é invertida. -#+begin_example -:~$ pidof salve-pipe -547187 -:~$ ls -l /proc/547187/fd -total 0 -lrwx------ 1 blau blau 64 jun 1 10:03 0 -> /dev/pts/1 -lrwx------ 1 blau blau 64 jun 1 10:03 1 -> /dev/pts/1 -lrwx------ 1 blau blau 64 jun 1 10:03 2 -> /dev/pts/1 -lr-x------ 1 blau blau 64 jun 1 10:03 3 -> pipe:[3331719] -l-wx------ 1 blau blau 64 jun 1 10:03 4 -> pipe:[3331719] -#+end_example +** Dígitos na tabela ASCII + +Os valores dos bytes da string ="123"= (além do terminador nulo ='\0'=, de valor +=0x00=) foram determinados pela convenção de representação de caracteres da +tabela ASCII: + +| Caractere | Hexadecimal | Octal | Decimal | +|-----------+-------------+-------+---------| +| =\0= | 0x00 | 0 | 0 | +| =0= | 0x30 | 060 | 48 | +| =1= | 0x31 | 061 | 49 | +| =2= | 0x32 | 062 | 50 | +| =3= | 0x33 | 063 | 51 | +| =4= | 0x34 | 064 | 52 | +| =5= | 0x35 | 065 | 53 | +| =6= | 0x36 | 066 | 54 | +| =7= | 0x37 | 067 | 55 | +| =8= | 0x38 | 070 | 56 | +| =9= | 0x39 | 071 | 57 | + +Em comparação aos valores dos caracteres em outras bases, a representação em +hexadecimal é bastante conveniente, pois basta subtrair =0x30= para saber qual +é o dígito correspondente. + +** Considerações sobre os tipos numéricos + +Em baixo nível, nós trabalhamos diretamente com tamanhos de dados. Os tipos +das linguagens de alto nível, como a linguagem C, são apenas abstrações para +ajudar a definir o tamanho dos dados e as regras de seus usos. Sendo assim, +existe uma equivalência entre as formas de representar tamanhos de dados em +Assembly com os tipos, por exemplo, da linguagem C. + +Considerando a arquitetura x86_64: + +| Tipo em C | Tamanho | Tamanho em Assembly | +|-----------+---------+---------------------| +| =char= | 1 byte | /byte/ | +| =short= | 2 bytes | /word/ | +| =int= | 4 bytes | /double-word/ | +| =long= | 8 bytes | /quad-word/ | +| =float= | 4 bytes | /double-word/ | +| =double= | 8 bytes | /quad-word/ | #+begin_quote -Observe as permissões =r= e =w= nos dois novos descritores de arquivos. +Esses tamanhos são especificados na ABI do System V AM64, usada pelo Linux +em arquitetura x86_64 e garantidos, na prática, pelo GCC e em sistemas +baseados na =glibc=. #+end_quote -A partir daqui, o programa deve clonar o processo pai (chamada =fork=) em um -novo processo filho, em seguida... +Em Assembly, portanto, definir uma lista de /double-words/ (=dd=) tem o mesmo +efeito de definir um vetor de inteiros na linguagem C, inclusive na ordem +em que os bytes são dispostos na memória. Ou então, o dado em um registrador +de 64 bits, se tomado como um todo, terá o tamanho de uma /quad-word/ e será +equivalente ao tipo =long= da linguagem C. -- No processo pai: - - Mover o descritor de escrita do pipe para a saída padrão; - - Escrever a mensagem no pipe; - - Aguardar o teŕmino do processo filho. -- No processo filho: - - Mover o descritor de leitura do pipe para a entrada padrão; - - Executar o utilitário =cat= para ler o pipe. +** Pesos posicionais -O efeito sobre os descritores de arquivos pode ser visto no próximo ponto -de parada, definido para antes da impressão da mensagem: +Falando apenas de inteiros sem sinal por enquanto, cada byte de uma string +numérica decimal representa um algarismo do número multiplicado pelo seu +/peso posicional/, por exemplo: #+begin_example -(gdb) c -Continuing. -[Detaching after fork from child process 549470] +0x31 0x32 0x33 => '1' '2' '3' -Breakpoint 2, _start.parada2 () at salve-pipe.asm:86 -86 mov rax, SYS_WRITE +0x31 - 0x30 * 100 = 100 (centenas) +0x32 - 0x30 * 10 = 20 (dezenas) +0x33 - 0x30 * 1 = 3 (unidades) + +100 + 20 + 3 = 123 #+end_example -No outro terminal, nós vamos repetir a listagem de =fd=: +Manualmente, este é um procedimento de conversão bastante simples, mas a sua +implementação em uma rotina de baixo nível não é tão intuitiva, embora também +não seja nada muito complicado. O /"pulo do gato"/ está em alcançar os pesos +posicionais de cada algarismo progressivamente: #+begin_example -:~$ ls -l /proc/547187/fd -total 0 -lrwx------ 1 blau blau 64 jun 1 10:03 0 -> /dev/pts/1 -l-wx------ 1 blau blau 64 jun 1 10:03 1 -> pipe:[3331719] -lrwx------ 1 blau blau 64 jun 1 10:03 2 -> /dev/pts/1 +031 0x32 033 => '1' '2' '3' + +val = 0x31 - 0x30 = 1 +val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 +val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 #+end_example -Como podemos ver no processo pai, os descritores =3= e =4= não existem mais e o -pipe está conectado apenas ao descritor de arquivos =1= (=stdout=). Para ver como -estão os descritores do processo do =cat=, vamos utilizar =pidof= na própria -linha do comando: +Ou, de uma forma mais regular: #+begin_example -:~$ ls -l /proc/$(pidof cat)/fd -total 0 -lr-x------ 1 blau blau 64 jun 1 10:36 0 -> 'pipe:[3331719]' -lrwx------ 1 blau blau 64 jun 1 10:36 1 -> /dev/pts/1 -lrwx------ 1 blau blau 64 jun 1 10:36 2 -> /dev/pts/1 +val = 0 +val = (val * 10) + (0x31 - 0x30) = 0 + 1 = 1 +val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 +val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 #+end_example -O que demonstra que o pipe está ligado ao descritor de arquivo =0= (=stdin=) e -o nosso programa conseguiu reproduzir perfeitamente o funcionamento do -mecanismo de pipe no shell. +Esse procedimento, facilmente implementável em Assembly, contempla tudo que +vimos sobre o problema: -** Demonstrando a leitura da entrada padrão +- Percorre os bytes da string na ordem em que estão na memória; +- Converte os dígitos ASCII para os valores que representam; +- Aplica os pesos posicionais corretos a cada algarismo encontrado. -O kernel fornece uma chamada de sistema para leitura da entrada padrão ou -de descritores de arquivos: a chamada =read=. Assim como o comando de mesmo -nome no shell, ao atuar sobre =stdin=, essa chamada pode receber dados de -forma interativa, ler arquivos por redirecionamento ou a saída de outro -comando encadeado por pipe. +Com isso, nós já podemos escrever uma rotina de conversão para strings que +representam números decimais sem sinal de todos os tipos compatíveis com +=int= (=char=, =short= e =long=, por exemplo). -Contudo, diferente do comando, a chamada de sistema não se limita à leitura -de uma linha. Em vez disso, ela tenta ler uma quantidade fixa de bytes e -escreve os dados lidos no endereço de memória especificado. Interativamente, -a leitura só é iniciada a partir do momento que o terminal envia os dados -digitados para o processo -- geralmente, quando o usuário tecla =Enter=. +* Conversão para inteiros sem sinal em alto nível -*** Uso da chamada de sistema 'read' +Em linguagem C, sem utilizar as funções especializadas da biblioteca padrão +e com base no procedimento que elaboramos, nós poderíamos implementar uma +função de conversão para inteiros sem sinal deste modo: -Na arquitetura x86_64, a chamada de sistema =read= é utilizada da seguinte -forma para ler a entrada padrão: - -#+begin_src asm -mov rax, 0 ; número da chamada de sistema read -mov rdi, FD ; Descritor de arquivo (0 = entrada padrão) -mov rsi, BUF ; O endereço do buffer em que os bytes serão escritos -mov rdx, COUNT ; A quantidade de bytes a ler -syscall ; executa a chamada de sistema +#+begin_src c +unsigned int str_to_uint(char *str) { + int dig; // Recebe o dígito convertido + unsigned int conv = 0; // Recebe a parcial da conversão + + for (int i = 0; str[i] != '\0'; i++) { + if (str[i] < '0' || str[i] > '9') break; + dig = str[i] - '0'; + conv = (conv * 10) + dig; + } + return conv; +} #+end_src -O retorno, passado por =rax=, é a quantidade de bytes lidos ou =-1= em caso -de erro. +Embora funcional para a maioria dos casos, essa abordagem tem alguns problemas: -*** Exemplo de uso em Assembly +- Não verifica se o valor convertido excede o limite máximo para inteiros sem + sinal da plataforma; +- Não verifica se o ponteiro para a string é nulo (=NULL=); +- Retorna =0= tanto para a conversão da string ="0"= quanto para uma string sem + dígitos nos primeiros caracteres. -O exemplo abaixo (=salve-read.asm=) demonstra como podemos receber dados -pela entrada padrão utilizando a chamada de sistema =read=. +Todos esses problemas poderiam ser solucionados com verificações e o retorno +de um valor de erro: mas que valor seria esse? Tipicamente, utilizaríamos =-1=, +mas o retorno da função tem que ser =unsigned=. Também poderíamos sacrificar um +número válido, como o valor do próprio limite máximo de inteiros sem sinal, +definido na =glibc= em =UINT_MAX=, no cabeçalho =limits.h=, mas isso requereria +uma nota sobre essa limitação. -Arquivo: [[exemplos/08/salve-read.asm][salve-read.asm]] +Uma terceira opção, que me parece bem mais razoável, seria incluir um segundo +parâmetro para receber o estado de término da função: + +#+begin_src c +unsigned int str_to_uint(char *str, int *err); +#+end_src + +Deste modo, a função poderia ser utilizada assim: + +#+begin_src c +char *str_num = "123"; // String a ser convertida. +int status; // Recebe o estado de término da função. + +unsigned int num = str_to_uint(str_num, &status); +if (!status) ...; // Consequências do erro... +#+end_src + +Para os casos de uso onde o tratamento de erros não for importante, nós +podemos dar a opção de passar =NULL= como argumento de =*err= e trabalhar a +implementação para definir um estado padrão =0= (erro), condicionado ao +ponteiro recebido, e alterá-lo para =1= somente antes de terminar com +sucesso: + +#+begin_src c +unsigned int str_to_uint(char *str, int *err /* nullable */) { + // Inicia a função com estado de erro, mas só se '*err' não for NULL... + if (err) *err = 0; + + // Outras verificações e rotina de conversão... + + // Altera o estado antes de terminar com sucesso (1)... + if (err) *err = 1; + return conv; +} +#+end_src + +Assim, caso =*err= seja =NULL=, nenhum estado de término será definido. + +** Erro de ponteiro nulo + +Caso o endereço da string (=*str=) seja passada como um ponteiro nulo, nós +simplesmente terminaremos a função com retorno =0= sem alterar o valor de +=*err=, que também é =0= (erro): + +#+begin_src c +// Inicia a função com estado de erro, mas só se '*err' não for NULL... +if (err) *err = 0; + +// Termina com erro (0) e valor 0 se str==NULL... +if (str == NULL) return 0; +#+end_src + +** Erro de byte inicial fora faixa de dígitos + +Apesar de ser uma verificação redundante, por ser repetida mais adiante no +código, ela é necessária para determinar quando a função retorna =0= por ter +convertido apenas uma string ="0"= ou devido a não haver dígitos para converter +no início da string, ou seja: + +#+begin_example +String: "abc123" --> Converte para 0 com estado 1 +String: "" --> Converte para 0 com estado 1 +String: "0" --> Converte para 0 com estado 1 +Erros --> Retornam conversão para 0, mas com estado 0 +#+end_example + +A ideia é que os dois primeiros casos (="abc123"= e =""=) retornem =0= om estado +de término =0= (erro), portanto... + +#+begin_src c +// Inicia a função com estado de erro, mas só se '*err' não for NULL... +if (err) *err = 0; + +// Termina com erro (0) e valor 0 se str==NULL... +if (str == NULL) return 0; + +// Termina com erro (0) e valor 0 se primeiro byte não for um dígito... +if (str[0] < '0' || str[0] > '9') return 0; +#+end_src + +Nós podemos, inclusive, combinar as duas verificações numa só: + +#+begin_src c +// Inicia a função com estado de erro, mas só se '*err' não for NULL... +if (err) *err = 0; + +// Termina com erro (0) e valor 0 se str==NULL +// ou se o primeiro byte não for um dígito... +if (str == NULL || str[0] < '0' || str[0] > '9') return 0; +#+end_src + +** Erro de estouro do limite do tipo + +Nós podemos utilizar o valor de =UINT_MAX= como referência para determinar se +o próximo valor parcial do ciclo de conversões excederá ou não o limite +máximo para números inteiros sem sinal. A cada ciclo, a conversão é feita +com: + +#+begin_example +valor = (valor * 10) + dígito; +#+end_example + +Para determinar se isso excederá o valor de =UNIT_MAX=, basta calcular qual +seria o valor corrente máximo para que a próxima conversão resulte em algo +dentro do limite: + +#+begin_example +valor * 10 + dígito < UINT_MAX +valor * 10 < UINT_MAX - dígito +valor < (UINT_MAX - dígito) / 10 +#+end_example + +No código, isso pode ser escrito no loop de conversão como: + +#+begin_src c +// Converte o dígito corrente... +dig = str[i] - '0'; + +// Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... +if (conv > (UINT_MAX - dig) / 10) return 0; + +// Processa a parcial da conversão... +conv = (conv * 10) + dig; +#+end_src + +** Implementação final em C + +Aqui está a implementação final, com as verificações e os procedimentos +atualizados: + +#+begin_src c +#include // Requerido para obter UINT_MAX da plataforma + +unsigned int str_to_uint(char *str, int *err /* nullable */) { + + if (err) *err = 0; // Estado padrão é de erro (0 = falso)! + + // Termina com erro e valor 0 se str==NULL ou se '0'>str[0]>'9'... + if (str == NULL || str[0] < '0' || str[0] > '9') return 0; + + int dig; // Recebe o dígito convertido + unsigned int conv = 0; // Recebe a parcial da conversão + + for (int i = 0; str[i] != '\0'; i++) { + // Se o caractere não for um dígito, termina a conversão... + if (str[i] < '0' || str[i] > '9') break; + + // Converte o dígito corrente... + dig = str[i] - '0'; + + // Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... + if (conv > (UINT_MAX - dig) / 10) return 0; + + // Processa a parcial da conversão... + conv = (conv * 10) + dig; + } + + if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) + return conv; +} +#+end_src + +** Exemplo de uso + +Arquivo: [[exemplos/08/uint.c][uint.c]] + +#+begin_src c :tangle exemplos/08/uint.c +/* + * Arquivo : uint.c + * Compilação: gcc -Wall uint.c -o uintc + */ +#include +#include // Requerido para obter UINT_MAX da plataforma + +unsigned int str_to_uint(char *str, int *err /* nullable */); + +int main(int argc, char **argv) { + if (argc == 1) { + fprintf(stderr, "Uso: %s NÚMERO\n", argv[0]); + return 1; + } + + int status; + unsigned int num = str_to_uint(argv[1], &status); + + if (status) { + printf("String: %s\nNúmero: %u\n", argv[1], num); + } else { + fprintf(stderr, "Erro de conversão!\n"); + return 1; + } + + return 0; +} + +unsigned int str_to_uint(char *str, int *err /* nullable */) { + if (err) *err = 0; // Estado padrão é de erro (0 = falso)! + + // Termina com erro e valor 0 se str==NULL ou se '0'>str[0]>'9'... + if (str == NULL || str[0] < '0' || str[0] > '9') return 0; + + int dig; // Recebe o dígito convertido + unsigned int conv = 0; // Recebe a parcial da conversão + + for (int i = 0; str[i] != '\0'; i++) { + // Se o caractere não for um dígito, termina a conversão... + if (str[i] < '0' || str[i] > '9') break; + // Converte o dígito corrente... + dig = str[i] - '0'; + // Termina com erro e valor 0 se a próxima conversão exceder UINT_MAX... + if (conv > (UINT_MAX - dig) / 10) return 0; + // Processa a parcial da conversão... + conv = (conv * 10) + dig; + } + + if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) + return conv; +} +#+end_src + +Compilação e testes: + +#+begin_example +:~$ gcc -Wall uint.c -o uintc +$ ./uintc +Uso: ./uintc NÚMERO +:~$ ./uintc 123 +String: 123 +Número: 123 +:~$ ./uintc 123abc +String: 123abc +Número: 123 +:~$ ./uintc $((2**32)) +Erro de conversão! +:~$ ./uintc $((2**32 - 1)) +String: 4294967295 +Número: 4294967295 +:~$ ./uintc '' +Erro de conversão! +:~$ ./uintc abc +Erro de conversão! +#+end_example + +* Conversão para inteiros sem sinal em baixo nível + +Em Assembly, o procedimento de conversão ainda é o mesmo: + +#+begin_example +val = 0 +val = (val * 10) + (0x31 - 0x30) = 0 + 1 = 1 +val = (val * 10) + (0x32 - 0x30) = 10 + 2 = 12 +val = (val * 10) + (0x33 - 0x30) = 120 + 3 = 123 +#+end_example + +Que pode ser implementado, de forma genérica, com uma sub-rotina assim: #+begin_src asm -; ---------------------------------------------------------- -; Arquivo : salve-read.asm -; Montagem: nasm -f elf64 salve-read.asm -; Ligação : ld -o salve-read salve-read.o -; ---------------------------------------------------------- -; Chamadas de sistema -; ---------------------------------------------------------- -%define SYS_READ 0 -%define SYS_WRITE 1 -%define SYS_EXIT 60 -; ---------------------------------------------------------- -; Descritores de arquivos padrão -; ---------------------------------------------------------- -%define STDIN_FD 0 -%define STDOUT_FD 1 -; ---------------------------------------------------------- -; Estados de término -; ---------------------------------------------------------- -%define EXIT_SUCCESS 0 -; ---------------------------------------------------------- -; Constantes simbólicas -; ---------------------------------------------------------- -%define BUF_SIZE 256 -; ---------------------------------------------------------- -section .rodata -; ---------------------------------------------------------- - msg db `Salve, simpatia!\nQual é a sua graça?\n` - msg_len equ $ - msg - - resp db "Falaê, " - resp_len equ $ - resp +_str_to_uitn: + ; ------------------------------------------------------ + ; Entrada: RSI = Endereço da string (char * str) + ; Saída : RAX = Valor convertido (unsigned int) + ; ------------------------------------------------------ + ; Preparação... + ; ------------------------------------------------------ + push rbx ; Salva conteúdo de rbx + xor rax, rax ; rax = 0 (acumula a conversão) + xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) + ; ------------------------------------------------------ +.conv_loop: + ; ------------------------------------------------------ + ; Condições de término do loop... + ; ------------------------------------------------------ + mov bl, [rsi] ; Lê o primeiro byte + test bl, bl ; Compara primeiro byte com o terminador '\0' + je .done ; Se 0x00, termina o loop + cmp bl, 0x30 ; Compara primeiro byte com '0' + jl .done ; Se menor, termina o loop + cmp bl, 0x39 ; Compara primeiro byte com '9' + jg .done ; Se maior, termina o loop + ; ------------------------------------------------------ + ; Conversão... + ; ------------------------------------------------------ + sub rbx, 0x30 ; Subtrai '0' do byte (dígito convertido) + imul rax, rax, 10 ; rax *= 10 (deslocamento do peso posicional) + add rax, rbx ; rax += dígito convertido + inc rsi ; Avança rdi para o próximo byte + jmp .conv_loop ; Volta ao início do loop de conversão + ; ------------------------------------------------------ +.done: + ; ------------------------------------------------------ + pop rbx ; Restaura rbx + ret +#+end_src - tail db `!\n` +Isso equivale à primeira versão da função em C e traz os mesmos problemas: + +- Não verifica se o valor convertido excede o limite máximo para inteiros sem + sinal da plataforma; +- Não verifica se o ponteiro para a string é nulo (=NULL=); +- Retorna =0= tanto para a conversão da string ="0"= quanto para uma string sem + dígitos nos primeiros caracteres. + +Além disso, se quisermos adaptar a sub-rotina para uso em um programa em C +(em um objeto compartilhado, por exemplo) ela precisa seguir as convenções +da ABI. Do jeito que está, ela já poderia ser utilizada, porque recebe o +primeiro argumento em =rdi= e retorna o valor convertido em =rax=, mas ainda +precisamos decidir como implementar a sinalização de erros. Na função em C, +isso é feito com um ponteiro opcional (/nullable/) para a variável que receberá +o estado de erro: + +#+begin_src c +unsigned int str_to_uint(char *str, int *err /* nullable */); +#+end_src + +A ABI determina que o segundo argumento deve ser recebido em =rdx=, então a +assinatura da sub-rotina passaria a ser: + +#+begin_src asm +_str_to_uitn: + ; ------------------------------------------------------ + ; Entrada: + ; RSI = Endereço da string (char * str) + ; RDX = Endereço da variável de erro (int *err) + ; Saída : + ; RAX = Valor convertido (unsigned int) + ; Se sucesso: *err = 1 (verdadeiro) + ; Se erro : *err = 0 (falso) + ; ------------------------------------------------------ +#+end_src + +Ainda na função em C, nós decidimos definir o estado de =*err= logo no início +e só alterá-lo antes do retorno com sucesso. Como o argumento pode receber +um ponteiro nulo (=NULL=), o estado de término foi definido condicionalmente: + +#+begin_src c +if (err) *err = 0; // Estado padrão é de erro (0 = falso)! +// ... +if (err) *err = 1; // Altera estado para sucesso (1 = verdadeiro) +return conv; +#+end_src + +Além disso, sempre que a função terminava com erro, o valor retornado era +definido com =0=, e isso também deve ser implementado na sub-rotina: + +#+begin_src asm +_str_to_uitn: + ; ------------------------------------------------------ + ; Entrada: + ; RSI = Endereço da string (char * str) + ; RDX = Endereço da variável de erro (int *err /* nullable */) + ; Saída : + ; RAX = Valor convertido (unsigned int) + ; Se sucesso: *err = 1 (verdadeiro) + ; Se erro : *err = 0 (falso) + ; ------------------------------------------------------ + ; Definição condicional do estado inicial de erro... + ; ------------------------------------------------------ + test rdx, rdx ; Se *err = NULL, rdx = 0 + jz .skip_error ; Se for 0, pula a definição do erro + mov dword [rdx], 0 ; *err = 0 => estado inicial é de erro (falso) + ; ------------------------------------------------------ +.skip_error: + ; ------------------------------------------------------ + + ; ... + + ; ------------------------------------------------------ +.error: + ; ------------------------------------------------------ + xor rax, rax ; Retorna 0 em caso de erro + jmp .done + ; ------------------------------------------------------ +.success: + ; ------------------------------------------------------ + ; Redefinição condicional do estado final de sucesso... + ; ------------------------------------------------------ + test rdx, rdx ; Se *err = NULL, rdx = 0 + jz .done ; Se for 0, pula a definição de sucesso + mov dword [rdx], 1 ; *err = 1 => estado final é de sucesso (verdadeiro) + ; ------------------------------------------------------ +.done: + ; ------------------------------------------------------ + pop rbx ; Restaura rbx + ret +#+end_src + +Com o procedimento de tratamento de erros definido, nós podemos prosseguir +com a implementação das validações necessárias. + +** Erro de ponteiro nulo + +A primeira verificação que faremos é para o caso de =rsi= ter recebido um +ponteiro nulo na chamada: + +#+begin_src asm + ; ------------------------------------------------------ + ; Teste de ponteiro nulo... + ; ------------------------------------------------------ + test rsi, rsi ; verifica se enderçeo é nulo (0) + jz .error ; se for, termina retornando -1 (erro) +#+end_src + +Deste modo, se =rsi= receber =0= (=NULL=, numa chamada em C), a sub-rotina salta +para o rótulo local =.error= e termina retornando =0= em =rax= e =1= no endereço +em =rdx=, se =*err= estiver definido. + +** Erro de byte inicial fora da faixa de dígitos + +Para que o primeiro byte seja verificado, ele precisa ser lido. Portanto, +a verificação da faixa de dígitos deve ser feita logo após a preparação +dos registradores utilizados, antes do /loop/ de conversão: + +#+begin_src asm + ; ------------------------------------------------------ + ; Preparação... + ; ------------------------------------------------------ + push rbx ; salva rbx na pilha + xor rax, rax ; rax = 0 (acumula o resultado) + xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) + mov bl, [rsi] ; lê o primeiro byte + ; ------------------------------------------------------ + ; Validação do primeiro byte... + ; ------------------------------------------------------ + cmp bl, 0x30 ; compara com menor byte válido + jl .error ; se menor, termina retornando -1 + cmp bl, 0x39 ; compara com maior byte válido + jg .error ; se maior, termina retornando -1 +#+end_src + +Assim, a sub-rotina salta para =.error= se o byte for menor que =0x30= (caractere +='0'=) ou maior que =0x39= (caractere ='9'=). + +** Erro de estouro do limite do tipo + +Como vimos, o cálculo do maior valor que pode ser multiplicado por 10 na +conversão é feito com: + +#+begin_example +(UINT_MAX - dígito) / 10 +#+end_example + +Contudo, essa verificação é um pouco mais complicada em Assembly, por alguns +motivos: + +- Nós não temos um limite definido para a plataforma, como a constante + simbólica =UINT_MAX=, da =glibc=. +- É possível definir =UINT_MAX= com uma macro no Assembly, mas não podemos + contar com isso no caso de reutilização da sub-rotina, a menos que ela + esteja em uma biblioteca que também contenha definições de macros. +- Também teremos que utilizar registradores auxiliares adicionais, porque, + numa divisão, =rax= recebe o quociente e =rdx= o resto, e ambos estão em + uso. + +Mas /"complicada"/ não é /"impossível"/, e nós podemos assumir alguns critérios: + +- A ABI define que inteiros terão 4 bytes (32 bits) em arquiteturas x86_64 + e as implementações da linguagem C no GCC e na Glibc garantem isso. +- O valor de =UINT_MAX= pode ser armazenado em =rcx= como =0xffffffff=, o maior + número sem sinal que pode ser escrito com 4 bytes. +- Os registradores =r8= e =r10= podem ser usados para salvar e restaurar os + dados em =rax= e =rdx=. +- O registrador =r9= pode ser usado para auxiliar nos cálculos. + +Portanto, esta é uma implementação possível: + +#+begin_src asm +; ------------------------------------------------------ +; Limite válido: conv < (UINT_MAX - dígito) / 10 +; ------------------------------------------------------ +mov r8, rax ; Salva parcial da conversão em r8 +mov r10, rdx ; Salva ponteiro de erro em r10 + +mov eax, -1 ; rax = 0x00000000ffffffff (UINT_MAX) +sub eax, ebx ; rax = UINT_MAX - dígito (dividendo) + +xor rdx, rdx ; prepara rdx para receber o resto da divisão +mov r9, 10 ; divisor +div r9 ; limite(rax) = (UINT_MAX - dígito) / 10 + +cmp r8, rax ; se parcial > limite, teremos um estouro +jg .error ; se maior, termina com erro + +mov rdx, r10 ; restaura rdx (pontiero para erros) +mov rax, r8 ; restaura rax (parcial da conversão) +#+end_src + +** Implementação final em Assembly + +Com alguns ajustes estéticos e na documentação, aqui está a implementação +completa... + +Arquivo: [[exemplos/08/uint.asm][uint.asm]] + +#+begin_src asm :tangle exemplos/08/uint.asm +; ---------------------------------------------------------- +; Arquivo : uint.asm +; Montagem: nasm -g -f elf64 uint.asm +; Ligação : ls uint.o -o uint +; ---------------------------------------------------------- +section .data +; ---------------------------------------------------------- + num_str db "123", 0 ; string numérica ; ---------------------------------------------------------- section .bss ; ---------------------------------------------------------- - buf resb BUF_SIZE ; Buffer de leitura - count resd 1 ; retorno de read (int) + num resd 1 ; 4 bytes para o valor UINT32 + status resd 1 ; 4 bytes para o estado de término ; ---------------------------------------------------------- section .text -; ---------------------------------------------------------- global _start +; ---------------------------------------------------------- _start: - ; Imprime a mensagem inicial - mov rsi, msg - mov rdx, msg_len - call _print - - ; Aguarda os dados da entrada padrão - mov rax, SYS_READ - mov rdi, STDIN_FD - mov rsi, buf - mov rdx, BUF_SIZE - syscall - - ; Salva retorno da chamada (bytes lidos) - mov [count], eax - - ; Imprime prefixo da resposta - mov rsi, resp - mov rdx, resp_len - call _print - - ; Imprime resposta - mov rsi, buf - mov rdx, [count] ; total de bytes lidos - dec rdx ; desconta \n - call _print - - ; Imprime final da resposta - mov rsi, tail - mov rdx, 2 ; !\n = 2 bytes - call _print - - ; Termina o programa - mov rax, SYS_EXIT - mov rdi, EXIT_SUCCESS - syscall ; ---------------------------------------------------------- -; Sub-rotinas... +; Teste de chamada (verificar com o GDB)... ; ---------------------------------------------------------- -_print: + mov rsi, num_str ; copia endereço da string em rsi + mov rdx, status ; copia o endereço do estado de erro em rdx + call _str_to_uint ; chama a sub-rotina de conversão + mov dword [num], eax ; copia o resultado como int para [num] ; ---------------------------------------------------------- - mov rax, SYS_WRITE - mov rdi, STDOUT_FD - syscall - ret -#+end_src - -*Montagem e execução:* - -#+begin_example -:~$ nasm -f elf64 salve-read.asm -:~$ ld -o salve-read salve-read.o -:~$ ./salve-read -Salve, simpatia! -Qual é a sua graça? -[Espera a digitação] -#+end_example - -Digitando, por exemplo, meu nome: - -#+begin_example -Blau -Falaê, Blau! -#+end_example - -*** Testes com pipe e redirecionamento - -O nosso exemplo também pode ler a entrada padrão se ela estiver ligada -a um arquivo, por redirecionamento, ou à ponta de leitura de um pipe, como -veremos nos próximos testes. - -*Leitura por pipe:* - -#+begin_example -:~$ echo Fulano de Tal | ./salve-read -Salve, simpatia! -Qual é a sua graça? -Falaê, Fulano de Tal! -#+end_example - -*Leitura de um arquivo:* - -#+begin_example -:~$ ./salve-read < /proc/sys/kernel/ostype -Salve, simpatia! -Qual é a sua graça? -Falaê, Linux! -#+end_example - -*Leitura em uma /here string/ (Bash):* - -#+begin_example -:~$ ./salve-read <<< 'Teste 123' -Salve, simpatia! -Qual é a sua graça? -Falaê, Teste 123! -#+end_example - -*Leitura de várias linhas (/here doc/):* - -#+begin_example -:~$ ./salve-read << FIM -> Linha 1 -> Linha 2 -> Linha 3 -> FIM -Salve, simpatia! -Qual é a sua graça? -Falaê, Linha 1 -Linha 2 -Linha 3! -#+end_example - -** Identificando a origem do fluxo de leitura - -Para determinar se a entrada padrão está ligada ou não a um terminal, nós -podemos utilizar a chamada de sistema =ioctl= (identificador =16=), que manipula -diversos parâmetros internos de dispositivos especiais, como os terminais. - -*Argumentos da chamada de sistema =ioctl=:* - -#+begin_src asm -mov rax, 16 ; número da chamada de sistema ioctl -mov rdi, FD ; descritor de arquivo (0 = stdin) -mov rsi, OP ; código da operação (TCGETS = 0x5401) -mov rdx, BUF ; NULL ou buffer para receber os parâmetros solicitados -syscall -#+end_src - -O código de operação que nos interessa é =TCGETS= (=0x5401=), que é a solicitação -das configurações do terminal associado ao descritor de arquivo em =rdi=. Se -não houver a ligação com um terminal, a chamada termina com erro =ENOTTY= -(/não é um TTY/) e retorna o inteiro =-1= em =rax=. Como só queremos testar o -retorno da chamada =ioctl=, nós não precisamos de um buffer e podemos passar -o valor =0= (=NULL=) para =rdx=. - -*Exemplo genérico de teste:* - -#+begin_src asm - mov rax, 16 ; chamada ioctl - mov rdi, 0 ; stdin - mov rsi, 0x5401 ; TCGETS - xor rdx, rdx ; 0 = NULL +_exit: +; ---------------------------------------------------------- + mov rax, 60 + mov rdi, 0 syscall - - cmp rax, 0 - jl .noy_a_tty ; rax < 0 -> não é um terminal - -.is_a_tty: - ; rotina se stdin for um terminal... - ; ... - jmp .end: - -.not_a_tty: - ; rotina se stdin não for um terminal... - ; ... -.end: - ; Rotinha de término... +; ---------------------------------------------------------- +; Sub-rotinas... +; ---------------------------------------------------------- +_str_to_uint: +; ---------------------------------------------------------- +; Converte string numérica em inteiro sem sinal (uint32_t). +; Entradas: +; RSI = Endereço da string (char *str) +; RDX = Endereço para estado de erro (int *err /* nullable */) +; Saída: +; RAX = valor convertido (uint32_t) ou 0, no caso de erro +; Sucesso: *rdx = 1 +; Erro: *rdx = 0 +; ---------------------------------------------------------- + ; Definição condicional do estado inicial de erro... + ; ------------------------------------------------------ + test rdx, rdx ; Se *err = NULL, rdx = 0 + jz .skip_error ; Se for 0, pula a definição do erro + mov dword [rdx], 0 ; *err = 0 => estado inicial é de erro (falso) + ; ------------------------------------------------------ +.skip_error: + ; ------------------------------------------------------ + ; Teste de ponteiro nulo... + ; ------------------------------------------------------ + test rsi, rsi ; verifica se enderçeo é nulo (0) + jz .error ; se for, termina retornando -1 (erro) + ; ------------------------------------------------------ + ; Preparação... + ; ------------------------------------------------------ + push rbx ; salva rbx na pilha + xor rax, rax ; rax = 0 (acumula o resultado) + xor rbx, rbx ; rbx = 0 (recebe os caracteres no loop) + mov bl, [rsi] ; lê o primeiro byte + ; ------------------------------------------------------ + ; Validação do primeiro byte... + ; ------------------------------------------------------ + cmp bl, 0x30 ; compara com menor byte válido + jl .error ; se menor, termina retornando -1 + cmp bl, 0x39 ; compara com maior byte válido + jg .error ; se maior, termina retornando -1 +.conv_loop: + ; ------------------------------------------------------ + ; Condições de término da conversão ... + ; ------------------------------------------------------ + test bl, bl ; verifica se o byte é o terminador 0x00 + je .success ; se for, termina com sucesso + cmp bl, 0x30 ; compara o byte em rbx com o menor dígito + jl .success ; se for menor, termina com sucesso + cmp bl, 0x39 ; compara o byte em rbx com o maior dígito + jg .success ; se for maior, termina com sucesso + ; ------------------------------------------------------ + ; Conversão do dígito corrente... + ; ------------------------------------------------------ + sub rbx, 0x30 ; converte o dígito para seu valor numérico + ; ------------------------------------------------------ + ; Limite válido: conv < (UINT_MAX - dígito) / 10 + ; ------------------------------------------------------ + mov r8, rax ; Salva parcial da conversão em r8 + mov r10, rdx ; Salva ponteiro de erro em r10 + + mov eax, -1 ; eax = 0xffffffff (UINT_MAX - 32 bits) + sub eax, ebx ; rax = UINT_MAX - dígito (dividendo) + + xor rdx, rdx ; prepara rdx para receber o resto da divisão + mov r9, 10 ; divisor + div r9 ; limite(rax) = (UINT_MAX - dígito) / 10 + + cmp r8, rax ; se parcial > limite, teremos um estouro + jg .error ; se maior, termina com erro + + mov rdx, r10 ; restaura rdx (pontiero para erros) + mov rax, r8 ; restaura rax (parcial da conversão) + ; ------------------------------------------------------ + ; Processa a parcial da conversão... + ; ------------------------------------------------------ + imul rax, rax, 10 ; rax *= 10 (deslocamento do peso posicional) + add rax, rbx ; rax += novo algarismo + inc rsi ; avança para o próximo byte + mov bl, [rsi] ; carrega o dígito corrente em rbx + jmp .conv_loop + ; ------------------------------------------------------ +.error: + ; ------------------------------------------------------ + xor rax, rax ; Retorna 0 em caso de erro + jmp .done + ; ------------------------------------------------------ +.success: + ; ------------------------------------------------------ + ; Redefinição condicional do estado final de sucesso... + ; ------------------------------------------------------ + test rdx, rdx ; Se *err = NULL, rdx = 0 + jz .done ; Se for 0, pula a definição de sucesso + mov dword [rdx], 1 ; *err = 1 => estado final é de sucesso (verdadeiro) + ; ------------------------------------------------------ +.done: + ; ------------------------------------------------------ + pop rbx ; Restaura rbx + ret #+end_src -* Criação e manipulação de fluxos de dados +** Teste com o GDB -Criar um novo fluxo de dados tem o significado de estabelecer uma nova -conexão entre um processo e um arquivo. Em alto nível, com C/C++ por exemplo, -isso é feito através das abstrações da =glibc=, o que pode resultar na ligação -com arquivos via /streams/ (por exemplo, com =fopen=) ou via descritores de -arquivos (por exemplo, com =pipe=). - -Exemplos: - -| Função | Descrição | -|-----------------+--------------------------------------------------| -| =fopen= | Abre um arquivo comum | -| =fclose= | Fecha um /stream/ | -| =freopen= | Redireciona um =FILE *= existente | -| =fdopen= | Associa um descritor de arquivo a um =FILE *= | -| =popen= | Executa um comando e abre um pipe unidirecional | -| =fmemopen= | Cria um =FILE *= associado a uma região da memória | -| =open_memstream= | Cria um =FILE *= de escrita que escreve em memória | -| =open_wmemstream= | Versão wide-char de =open_memstream= | - -Já em baixo nível, como vimos nos nossos exemplos, a criação de novos fluxos -de dados é feita pelas chamadas de sistema do kernel e sempre resulta na -ligação com arquivos via descritores de arquivos. - -Exemplos (Linux x86_64): - -| Nº | Syscall | Descrição | -|-----+------------+----------------------------------------------------| -| 2 | =open= | Abre um arquivo e retorna um =file descriptor= | -| 3 | =close= | Fecha um descritor de arquivo | -| 85 | =creat= | Cria/abre arquivo para escrita | -| 41 | =dup= | Duplica um descritor de arquivo | -| 33 | =dup2= | Duplica descritor para um valor específico | -| 292 | =dup3= | Como =dup2=, mas com flags | -| 22 | =pipe= | Cria um pipe anônimo (=int pipefd[2]=) | -| 293 | =pipe2= | Versão de =pipe= com flags adicionais | -| 41 | =socket= | Cria endpoint de comunicação (AF_INET, AF_UNIX...) | -| 53 | =socketpair= | Cria par de sockets conectados | - -* Armazenamento temporário (/bufferização/) - -Bufferização (*buffering*) é a técnica de usar áreas intermediárias de memória -chamadas *buffers* para armazenar temporariamente dados durante operações de -entrada e saída, principalmente com o propósito de: - -- Reduzir o número de chamadas de sistema. -- Adaptar diferenças de velocidade entre a produção e o consumo de dados. -- Agrupar dados em blocos maiores, aumentando a eficiência da comunicação - com dispositivos ou arquivos. - -** Bufferização em alto nível - -No GNU/Linux, a =glibc= implementa três modos de bufferização: - -| Tipo | Descrição | -|------------------------+----------------------------------------------------------------| -| *Sem bufferização* | Dados são enviados diretamente, sem armazenar em buffer. | -| *Bufferização por linha* | Dados são enviados quando uma nova linha é detectada (=\n=). | -| *Bufferização total* | Dados são armazenados até o buffer ficar cheio ou ser liberado | - -Os três fluxos padrão estão associados a buffers internos mantidos pela =glibc=: - -- *stdin*: bufferizado totalmente. -- *stdout*: bufferizado por linha se conectado a um terminal; caso contrário, totalmente. -- *stderr*: sem bufferização. - -Esses buffers fazem parte da estrutura =FILE=, da biblioteca C padrão, que -aloca uma região de memória para guardar dados temporariamente. - -** Bufferização no nível do kernel - -Além da =glibc=, o kernel Linux também implementa buffers internos em diversos -contextos: - -| Tipo de buffer | Onde ocorre | Finalidade principal | -|---------------------+--------------------------+-----------------------------------------------| -| Cachê de página | Sistema de arquivos | Acelerar leitura/escrita em disco | -| Buffer de pipe | IPC (pipe) | Comunicação entre processos | -| Buffer de socket | Rede (TCP/UDP) | Armazenar pacotes antes/depois da transmissão | -| Disciplina de linha | Dispositivos de terminal | Controlar entrada interativa | -| Buffer de blocos | Dispositivos de bloco | Otimização de leitura/escrita de disco | - -*** Cachê de página - -O subsistema de arquivos do kernel utiliza cachês de página (/page cache/) -para manter dados recentemente lidos ou escritos na memória RAM. Isso evita -acessos frequentes ao disco, que são muito mais lentos. Quando um processo -executa =read=, o kernel verifica se os dados já estão no cache; quando um -processo executa =write=, os dados vão para o cache e são escritos no disco -posteriormente (/write-back/). - -#+begin_quote -Esse mecanismo é transparente para o usuário e só afeta o desempenho. -#+end_quote - -*** Buffers de pipes (IPC) - -O kernel mantém buffers internos para comunicação entre processos (IPC) -via pipes, que possuem um buffer circular (/ring buffer/) para armazenar -dados de forma contínua em um espaço fixo de memória (tipicamente, 64kB -em sistemas modernos). Ao escrever num pipe, os dados vão para esse -buffer e são consumidos quando acontece a leitura. Se o buffer estiver -cheio, a escrita é bloqueada; se estiver vazio, a leitura é bloqueada. - -#+begin_quote -Esta condição padrão é chamada de /modo bloqueante/, mas também é possível -abrir arquivos em /modo não bloqueante/ se estivermos lidando com eventos -simultâneos, escrevendo programas que não podem parar esperando operações -de entrada e saída ou se precisarmos implementar um controle fino sobre -o fluxo de dados. -#+end_quote - -*** Buffers de sockets - -Comunicações via /sockets/ também contam com buffers internos no kernel. Cada -socket possui um buffer de envio (/send buffer/) e um recepção (/receive buffer/) -para armazenar dados enquanto o pacote é processado, retransmitido ou -recebido pela outra extremidade. - -#+begin_quote -O tamanho dos buffers pode ser consultado com a chamada de sistema =getsockopt= -ou definido com a chamada =setsockopt=. -#+end_quote - -*** Buffers de terminais (TTY) - -Os dispositivos de terminal possuem buffers no kernel que são manipulados -pelo driver de TTY. No /modo canônico/ (também chamado de /modo linha/), os -dados são acumulados até que o usuário tecle =Enter=. Além disso, o terminal -é responsável pela edição a linha antes que ela seja entregue ao processo. - -#+begin_quote -Esse buffer faz parte da /disciplina de linha/ do kernel, um componente que -fica entre o driver do terminal e o processo do usuário para interpretar -e gerenciar entradas e saídas de texto. -#+end_quote - -As principais características do modo canônico, que é o modo padrão de -terminais interativos, são: - -- Entrada bufferizada por linha. -- Possibilidade de edição das linhas antes do envio. -- Interpretação de caracteres de controle. - -No modo não canônico (modo /bruto/ ou /"caractere a caractere"/), a entrada -não é bufferizada e, portanto, cada caractere digitado é disponibilizado -imediatamente para o programa. Isso é muito utilizado por programas com -interfaces em tela cheia (como o editor Vim, por exemplo), multiplexadores -de terminal (como o Tmux e o GNU Screen) e pseudo terminais remotos -(como no SSH). - -*** Buffers de blocos (bloco de disco) - -Dispositivos de bloco (como discos) usam buffers no kernel para: - -- Agrupar operações de escrita. -- Reordenar acessos. -- Sincronizar acesso com caches do sistema de arquivos. - -** Bufferização em Assembly (x86_64) - -Em Assembly, os buffers são criados manualmente com a reserva de espaço -na seção =.bss= (=resb=, =resq=, etc), utilizando a pilha ou alocando espaços -dinamicamente, por exemplo, com as chamadas de sistema =brk= e =mmap=. - -No último exemplo (=salve-read.asm=), foi o que fizemos quando reservamos -256 bytes (valor de =BUF_SIZE=) em =.bss= para armazenar os caracteres lidos -pela chamada =read=: +Nosso objetivo é testar se a string em =num_str= é convertida corretamente +pela análise dos valores em =rax= e =rdx= (=status=) após a chamada da sub-rotina +neste trecho do programa: #+begin_src asm ; ---------------------------------------------------------- +section .data +; ---------------------------------------------------------- + num_str db "123", 0 ; string numérica +; ---------------------------------------------------------- section .bss ; ---------------------------------------------------------- - buf resb BUF_SIZE ; Buffer de leitura - count resd 1 ; retorno de read (int) + num resd 1 ; 4 bytes para o valor UINT32 + status resd 1 ; 4 bytes para o estado de término +; ---------------------------------------------------------- +section .text +global _start +; ---------------------------------------------------------- +_start: +; ---------------------------------------------------------- +; Teste de chamada (verificar com o GDB)... +; ---------------------------------------------------------- + mov rsi, num_str ; copia endereço da string em rsi + mov rdx, status ; copia o endereço do estado de erro em rdx + call _str_to_uint ; chama a sub-rotina de conversão + mov dword [num], eax ; copia o resultado como int para [num] #+end_src -Essa mesma técnica é aplicável a programas em C, geralmente utilizando -vetores de caracteres ou alocando espaço no /heap/ com funções como =malloc=, -da biblioteca padrão. +*Montagem e execução no GDB:* + +#+begin_example +:~$ nasm -g -f elf64 uint.asm +:~$ ld uint.o -o uint +:~$ gdb ./uint +Reading symbols from ./uint... +(gdb) break _start +Breakpoint 1 at 0x401000: file uint.asm, line 22. +(gdb) run +Starting program: /home/blau/git/pbn/curso/exemplos/08/uint + +Breakpoint 1, _start () at uint.asm:22 +22 mov rsi, num_str ; copia endereço da string em rsi +(gdb) +#+end_example + +*Execução e verificações antes da chamada:* + +#+begin_example +(gdb) n 2 +24 call _str_to_uint ; chama a sub-rotina de conversão +(gdb) info registers rax rdx rsi +rax 0x0 0 +rdx 0x402008 4202504 +rsi 0x402000 4202496 +(gdb) x /1wx &num_str +0x402000 : 0x00333231 +(gdb) x /1s &num_str +0x402000 : "123" +(gdb) x /1wx &num_ +No symbol "num_" in current context. +(gdb) x /1wx &num +0x402004 : 0x00000000 +(gdb) x /1wx &status +0x402008 : 0x00000000 +#+end_example + +*Execução e verificações após a chamada:* + +#+begin_example +(gdb) n +25 mov dword [num], eax ; copia o resultado para [num] +(gdb) n +_exit () at uint.asm:29 +29 mov rax, 60 +(gdb) info registers rax rdx rsi +rax 0x7b 123 +rdx 0x402008 4202504 +rsi 0x402003 4202499 +(gdb) x /1wx &num +0x402004 : 0x0000007b +(gdb) print (int)num +$1 = 123 +(gdb) x /1wx &status +0x402008 : 0x00000001 +#+end_example + +* Conversão para inteiros com sinal + +No Assembly x86_64, números com sinal são representados usando o método do +/complemento de dois/. Tomando um inteiro de 32 bits como exemplo, se o bit +mais significativo (MSB, o bit mais a esquerda) for =0=, o número é interpretado +como positivo ou zero; se for =1=, o número é negativo. Em outras palavras, o +bit mais a esquerda de um número é o seu sinal. + +O complemento de dois de um número binário é obtido pela inversão de todos +os seus bits (com uma operação =NOT= bit a bit) seguida da soma de 1: + +#+begin_example + 00000000 00000000 00000000 00000101 = 5 (0x0005) +NOT -> 11111111 11111111 11111111 11111010 = -6 (0xFFFA) + +1 -> 11111111 11111111 11111111 11111011 = -5 (0xFFFB) +#+end_example + +** Interpretação do sinal + +Se o MSB de um número for =1=, o que acontecerá sempre que o byte (não bit) +mais significativo de seu valor em hexadecimal iniciar com =8= ou mais, o +número poderá ser interpretado como negativo ou positivo -- tudo depende +do contexto. Por exemplo, o número 2.147.483.648, se escrito com 4 bytes +em hexa, será =0x80000000= e seu equivalente binário será: + +#+begin_example +10000000 00000000 00000000 00000000 = 0x80 00 00 00 = 2.147.483.648 +#+end_example + +Digamos que esse número seja escrito em =edx= (32 bits) e seja utilizado em +uma chamada de função que espera um argumento inteiro sem sinal. Neste caso, +o valor será tomado como positivo, mesmo que o binário inicie com =1=. Porém, +considere esta situação: + +#+begin_src asm +mov eax, 0x80000000 +cmp eax, 0 +#+end_src + +A instrução =cmp= calcula =eax - 0= e configura as flags de acordo com o valor +em =eax= e o resultado da operação: + +- Flag =SF=: foi definida com =1= porque o MSB do resultado é =1= (negativo). +- Flag =OF=: como não houve /overflow/ na operação, seu valor foi definido como =0=. +- Flag =CF=: como a operação não provocou um "vai um" ou um empréstimo fora + do limite representável em 32 bits (capacidade de =eax=), seu valor foi + definido como =0=. + +Neste ponto, a interpretação do sinal do valor em =eax= vai depender do que +faremos com o resultado da comparação. Observe esta possibilidade: + +#+begin_src asm + mov eax, 0x80000000 + cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0 + jl .menor ; Se SF != OF, o salto acontece + ; ... +.menor: + ; ... +#+end_src + +A instrução =jl= (/jump if less/) avalia se as flags =SF= e =OF= são diferentes: se +forem, o salto é feito. Portanto, =jl= levaria em conta o sinal do resultado +da subtração e salto aconteceria porque =eax < 0=. Mas, e neste caso? + +#+begin_src asm + mov eax, 0x80000000 + cmp eax, 0 ; resulta 0x80000000 -> SF=1 e OF=0 + jb .menor ; Se CF = 1, o salto acontece + ; ... +.menor: + ; ... +#+end_src + +Já a instrução =jb= (/jump if below/) só salta se =CF=1=, mas o resultado da +subtração (=eax-0=) não provocou um "vai um" ou um empréstimo fora do limite +do registrador: portanto, seu valor será =0= e o salto não acontecerá. Isso +significa que, como =jb= não leva em conta o sinal, =0x80000000= foi interpretado +como um valor acima de zero. + +Por outro lado, se utilizássemos =rax= em vez de =eax=... + +#+begin_src asm + mov rax, 0x80000000 ; 0x0000000080000000 + cmp rax, 0 ; CF=0 SF=0 e OF=0 + jb .menor ; CF=0, não salta + jl .menor ; SF=OF, não salta + ; ... +.menor: + ; ... +#+end_src + +Desta forma, não há ambiguidades quanto à interpretação do sinal. + +** Uma nota sobre as flags + +É importante entender corretamente o conceito por detrás da /carry flag/, +porque, em operações sem sinal, é ela que indica se um número é menor do +que outro. + +Então, vejamos: =10= é menor do que =14=? + +#+begin_src asm +mov al, 10 ; 10 = 1010 = 0x0A +cmp al, 14 ; 14 = 1110 = 0xFE +#+end_src + +A instrução =cmp= fará a subtração... + +#+begin_example + Aqui, temos que fazer um empréstimo e o MSB de 10 será 0... + ↓ + 1010 +- 1110 +------ + 00 + + Agora é o MSB que requer um empréstimo! + ↓ + 0110 +- 1110 +------ + ?100 +#+end_example + +Numa situação desas, onde não há mais de onde emprestar um bit =1= mais a +esquerda, o processador efetua a subtração, como se houvesse de onde tirar +o bit 1 de que precisa, sobe a flag =CF= (indicando que houve um empréstimo +fora do limite dos operandos) e a flag =SF= (indicando que o resultado é +negativo). Ao mesmo tempo, =OF= e =ZF= receberiam =0=, porque não houve estouro +do limite do valor representável com sinal e o resultado não foi zero. + +Na operação acima, nós teríamos: + +#+begin_example + CF=1 + ↓ + (1)0110 + - 1110 + ------ + 1100 = -4 ← ZF=0 (não é zero) + ↑ + SF=1 + OF=0 (é possível escrever em 4 bits) +#+end_example + +#+begin_quote +O processador não possui circuitos capazes de efetuar subtrações e, por isso, +não executaria a operação desta forma, e sim calculando o complemento de dois +de =-14= (=0010=) para, em seguida, somar com =10=: ~1010+0010 = 1100 (-4)~. +#+end_quote + +** Limites de inteiros com sinal + +Assumindo que inteiros são escritos com 4 bytes, o maior valor absoluto +que podemos representar ainda é =0xFFFFFFFF=, mas ele teria que ser visto +como um número negativo. Calculando seu complemento de dois... + +#+begin_example + 11111111 11111111 11111111 11111111 (0xFFFFFFFF) +NOT -> 00000000 00000000 00000000 00000000 + +1 -> 00000000 00000000 00000000 00000001 => (1) +#+end_example + +Portanto, levando em conta o sinal, =0xFFFFFFFF= é =-1=. + +Como o bit mais significativo representa o sinal, o primeiro dígito do byte +mais significativo do maior número positivo que pode ser escrito em hexa tem +que ser =7=: + +#+begin_example +Maior byte positivo: 0x7f = 0111 1111 = 127 +#+end_example + +Se somarmos =1= a =0x7F=, nós chegaremos a =0x80= (128), que será interpretado como +um número negativo: + +#+begin_example +0x7F -> 0111 1111 + +1 -> 1000 0000 = 0x80 = -128 + ↑ ↑ + SF=1 -------------´ +#+end_example + +Então, se o inteiro com sinal tiver 1 byte, seus limites serão: + +#+begin_example +0x7F = 0111 1111 = +127 +0x7E = 0111 1110 = +126 +... +0x01 = 0000 0001 = +1 +0x00 = 0000 0000 = 0 +0xFF = 1111 1111 = -1 +... +0x81 = 1000 0001 = -127 +0x80 = 1000 0000 = -128 +#+end_example + +Transpondo o princípio para o tamanho de um inteiro na arquitetura x86_64, +os limites são: + +#+begin_example +0x7FFFFFFF -> 2.147.483.647 +0x80000000 -> -2.147.483.648 +#+end_example + +** Procedimento de conversão para inteiros com sinal + +As diferenças em relação à conversão de strings para inteiros sem sinal são: + +- Antes da conversão, nós precisamos detectar a presença dos caracteres + de sinal (=+= e =-=); +- Depois do valor absoluto encontrado, nós teremos que verificar se ele está + na faixa de valores inteiros com sinal (entre =0x7FFFFFFF= e =0x80000000=); +- No final, se o número passar na verificação, nós teremos que calcular seu + complemento de dois, o que pode ser feito multiplicando o valor por =-1= ou, + de forma ainda mais compacta e eficiente, utilizando a instrução =neg=. + +Todo o restante do procedimento é idêntico, tanto que podemos reutilizar +as mesmas funções de antes para a etapa de conversão. + +** Conversão em alto nível + +Com base no procedimento geral, esta seria uma forma de implementar a +conversão de inteiros com sinal em linguagem C reutilizando a função que +criamos para converter inteiros sem sinal: + +#+begin_src c +#include // Requerido para obter INT_MAX e INT_MIN da plataforma + +int str_to_sint(char *str, int *err) { + if (err) *err = 0; // Flag de estado de término: 0 = erro + if (str == NULL) return 0; // Teste do ponteiro para a string + + // Verificação e atrubuição do sinal da string... + char sig = '+'; // Sinal padrão + if (str[0] == '+' || str[0] == '-') { + sig = str[0]; + str++; // Avança o ponteiro da string + } + + // Chama a função de conversão de inteiros sem sinal... + unsigned conv = str_to_uint(str, err); + if (!*err) return 0; + + // Verificação da faixa de inteiros com sinal... + if (sig == '-') { + // INT_MIN=-2147483648, que em unsigned é 2147483648 + if (conv > (unsigned)INT_MAX + 1) return 0; + if (err) *err = 1; // Flag de estado de término: 1 = sucesso + return -(int)conv; + } else { + if (conv > INT_MAX) return 0; + if (err) *err = 1; // Flag de estado de término: 1 = sucesso + return (int)conv; + } +} +#+end_src + +#+begin_quote +Agora cabe a você, como parte dos exercícios propostos, elaborar e executar +os testes e os ajustes, se forem necessários. +#+end_quote + +** Conversão em baixo nível + +Esta seria uma solução equivalente implementada como uma sub-rotina que +depende de =_str_to_uint=, criada e testada anteriormente: + +#+begin_src asm +; ---------------------------------------------------------- +_str_to_sint: +; ---------------------------------------------------------- +; Converte string numérica com sinal em inteiro (int32_t). +; Entradas: +; RSI = Endereço da string (char *str) +; RDX = Endereço para estado de erro (int *err /* nullable */) +; Saída: +; RAX = valor convertido (int32_t) +; Sucesso: *rdx = 1 +; Erro: *rdx = 0 +; ---------------------------------------------------------- + push rbx ; salva rbx + push rcx ; salva rcx + xor rcx, rcx ; rcx = 0 (sinal: 0 = positivo, 1 = negativo) + ; ------------------------------------------------------ + ; Testa ponteiro nulo + ; ------------------------------------------------------ + test rsi, rsi + jz .error + ; ------------------------------------------------------ + ; Verifica o sinal na string + ; ------------------------------------------------------ + mov bl, [rsi] + cmp bl, '-' ; caractere de sinal negativo? + jne .check_plus + inc rcx ; rcx = 1 => negativo + inc rsi ; avança o ponteiro + jmp .convert +.check_plus: + cmp bl, '+' ; caractere de sinal positivo? + jne .convert + inc rsi ; ignora '+' +.convert: + ; ------------------------------------------------------ + ; Chama _str_to_uint + ; ------------------------------------------------------ + call _str_to_uint ; RSI = str, RDX = err, RAX = resultado (uint32_t) + ; ------------------------------------------------------ + ; Verifica se _str_to_uint retornou erro + ; ------------------------------------------------------ + test rdx, rdx + jz .check_sign + cmp dword [rdx], 0 + je .error +.check_sign: + test rcx, rcx ; se rcx = 1, é número negativo + jz .check_range_pos + ; ------------------------------------------------------ + ; Verifica se valor cabe em int32_t negativo + ; ------------------------------------------------------ + cmp eax, 0x80000000 + ja .error ; se maior que INT_MAX + 1 -> erro + + neg eax ; aplica o sinal negativo + jmp .success +.check_range_pos: + ; ------------------------------------------------------ + ; Verifica se valor cabe em int32_t positivo + ; ------------------------------------------------------ + cmp eax, 0x7fffffff + ja .error ; maior que INT_MAX -> erro +.success: + test rdx, rdx + jz .done + mov dword [rdx], 1 ; *err = 1 + jmp .done +.error: + xor eax, eax ; valor de erro = 0 + test rdx, rdx + jz .done + mov dword [rdx], 0 ; *err = 0 +.done: + pop rcx + pop rbx + ret +#+end_src + +#+begin_quote +Também faz parte dos exercícios propostos, elaborar e executar os testes +e os ajustes, se forem necessários. +#+end_quote * Exercícios propostos -1. Altere o tamanho do buffer do programa =salve-read.asm= para =10= (bytes), - observe seu novo funcionamento e responda: - - O que acontece quando digitamos mais de 10 caracteres? - - Como você explica o que aconteceu? -2. Ainda usando =salve-read.asm= como base, faça com que o programa só funcione - no modo interativo, imprimindo uma mensagem por =stderr= e terminando com - estado =1= caso esteja num pipe ou com a entrada padrão redirecionada para - um arquivo. -3. Pesquise e escreva um programa em C que reproduza o funcionamento do exemplo - =salve-pipe.asm=. -4. Pesquise e escreva programas em C que reproduzam o funcionamento do exemplo - =salve-read.asm= e de todas as alterações feitas nos exercícios 1 e 2. +1. Tanto a função quanto a sub-rotina têm alguns problemas práticos não contemplados, como: + - Não tratam strings iniciadas com espaços em branco; + - Não tratam números com zeros à esquerda. + Inclua esses tratamentos em ambas as implementações de conversão. +2. Altere o valor em =num_str=, nos testes da sub-rotina =_str_to_uint=, e + analise os resultados com o GDB. +3. Em C, crie as funções =str_to_ulong= (string para inteiros longos sem sinal) + e =str_to_slong= (string para inteiros longos com sinal). +4. Em Assembly, crie as sub-rotinas =_str_to_ulong= e =_str_to_slong=. +5. Implemente predicados (funções que retornam verdadeiro ou falso) para detectar: + - Se a string representa um número (=is_num=); + - Se o número representado é do tipo =int= (=is_int=); + - Se o número representado é do tipo =long= (=is_long=); + - Se o número representado é positivo (=is_pos=). +6. Só para aproveitar o embalo, crie uma função para detectar se uma cadeia de + caracteres é uma string (=is_string=). -* Tabela das chamadas de sistema utilizadas - -| Chamada | #ID | Propósito | rax | rdi | rsi | rdx | -|---------+-----+-------------------------------------+-----+----------------------+----------------+-------------| -| =read= | 0 | Ler um descritor de arquivo | 0 | fd (ex: STDIN=0) | buffer | quantidade | -| =write= | 1 | Escrever em um descritor de arquivo | 1 | fd (ex: STDOUT=1) | buffer | tamanho | -| =open= | 2 | Abre um arquivo | 2 | nome do arquivo | flags | modo | -| =close= | 3 | Fechar descritor de arquivo | 3 | fd | - | - | -| =ioctl= | 16 | Obter parâmetros de dispositivos | 16 | fd | operação | buffer | -| =pipe= | 22 | Criar pipe (par de descritores) | 22 | ponteiro pipefd[2] | - | - | -| =dup2= | 33 | Duplicar descritor de arquivo | 33 | oldfd | newfd | - | -| =fork= | 57 | Criar novo processo | 57 | - | - | - | -| =execve= | 59 | Executar novo programa | 59 | caminho ("/bin/cat") | argv | envp | -| =exit= | 60 | Encerrar o processo | 60 | código de saída | - | - | -| =wait4= | 61 | Esperar término de processo filho | 61 | pid (-1 = qualquer) | statloc = NULL | options = 0 | - -Retorno em =rax=. - -* Referências - -- =man 2 read= -- =man 2 write= -- =man 2 open= -- =man 2 close= -- =man 2 ioctl= -- =man 2 pipe= -- =man 2 dup2= -- =man 2 fork= -- =man 2 execve= -- =man 2 wait4= -- =man 2 waitpid= -- =man 3 stdin= -- =man 5 proc_pid_fd= -- [[https://en.wikipedia.org/wiki/Pipeline_(Unix)][Wikipedia: Pipeline (Unix)]]