<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ciencia-Da-Computacao | AkitaOnRails.com</title><link>https://akitaonrails.github.io/tags/ciencia-da-computacao/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Mon, 19 Oct 2026 23:20:48 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/tags/ciencia-da-computacao/index.xml" rel="self" type="application/rss+xml"/><item><title>[Akitando #140] Desbloqueando o "Algoritmo" do Twitter - Introdução a Grafos</title><link>https://akitaonrails.github.io/2023/04/11/akitando-140-desbloqueando-o-algoritmo-do-twitter-introducao-a-grafos/</link><guid isPermaLink="true">https://akitaonrails.github.io/2023/04/11/akitando-140-desbloqueando-o-algoritmo-do-twitter-introducao-a-grafos/</guid><pubDate>Tue, 11 Apr 2023 08:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/uIflMYQnp8A"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;O Twitter liberou parte do código que chamou de &amp;ldquo;The Algorithm&amp;rdquo;, que forma o sistema que popula a aba de recomendações &amp;ldquo;Para Você&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Vamos entender qual é o contexto de ciência da computação e história necessários pra começar a entender esse código todo. E uma pequena introdução à ciência por trás de recomendações e grafos em geral.&lt;/p&gt;
&lt;p&gt;Finalmente, vou poder mostrar na prática a diferença de &amp;ldquo;código de curso&amp;rdquo; e &amp;ldquo;código de verdade&amp;rdquo; e exatamente porque software nunca acaba.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/uIflMYQnp8A"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>O Twitter liberou parte do código que chamou de &ldquo;The Algorithm&rdquo;, que forma o sistema que popula a aba de recomendações &ldquo;Para Você&rdquo;.</p>
<p>Vamos entender qual é o contexto de ciência da computação e história necessários pra começar a entender esse código todo. E uma pequena introdução à ciência por trás de recomendações e grafos em geral.</p>
<p>Finalmente, vou poder mostrar na prática a diferença de &ldquo;código de curso&rdquo; e &ldquo;código de verdade&rdquo; e exatamente porque software nunca acaba.</p>
<h2>Capítulos<span class="hx:absolute hx:-mt-20" id="capítulos"></span>
    <a href="#cap%c3%adtulos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00:00 - Intro</li>
<li>00:00:57 - CAP 1 - Avisos e Descrevendo o Repositório</li>
<li>00:02:51 - CAP 2 - Interpretando Errado: Não Acredite em Blog Posts</li>
<li>00:07:14 - CAP 3 - Derrubando o Meme: Pessoas Idiotas</li>
<li>00:11:30 - CAP 4 - Objetivos do Video: derrubar os Clones. Código de Verdade</li>
<li>00:16:37 - CAP 5 - Entendendo Grafos. Introdução a Relevância</li>
<li>00:21:47 - CAP 6 - Primeira Geração de Relevância: WTF</li>
<li>00:27:49 - CAP 7 - Relevância nos Primórdios da Web: PageRank e HITS</li>
<li>00:35:43 - CAP 8 - Segunda Geração de Relevância: RealGraph e SALSA</li>
<li>00:39:22 - CAP 9 - Escalando com Hadoop e MapReduce: HDFS e Pig</li>
<li>00:43:15 - CAP 10 - Algoritmos Escaláveis de Grafos: Relembrando Mergesort</li>
<li>00:46:58 - CAP 11 - Terceira Geração de Relevância: GraphJet</li>
<li>00:50:36 - CAP 12 - Earlybird: Light Ranker. Apache Lucene</li>
<li>00:51:37 - CAP 13 - Ids para Sistemas Distribuídos: Snowflake</li>
<li>00:55:23 - CAP 14 - Serialização Eficiente sem JSON: Thrift e Protobufs</li>
<li>01:01:07 - CAP 15 - A Controvérsia do &ldquo;Rails Não Escala&rdquo;. Por que Scala?</li>
<li>01:07:37 - CAP 16 - Introdução a Similarity Clusters: Similaridade de Cosseno</li>
<li>01:16:08 - CAP 17 - Resumindo os demais Projetos: Aplicando o que Aprendemos</li>
<li>01:20:22 - CAP 18 - Qualidade do Código: Mini-Assessment</li>
<li>01:22:43 - CAP 19 - Sendo Educado na Web: Memes Idiotas</li>
<li>01:24:32 - CAP 20 - Refletindo sobre o Código: Lições a Aprender</li>
<li>01:27:31 - Bloopers</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Fio no Twitter (<a href="https://twitter.com/AkitaOnRails/status/1641887891327537158?s=20"target="_blank" rel="noopener">https://twitter.com/AkitaOnRails/status/1641887891327537158?s=20</a>)</li>
<li>Twitter: The Algorithm (<a href="https://github.com/twitter/the-algorithm"target="_blank" rel="noopener">https://github.com/twitter/the-algorithm</a>)</li>
<li>Twitter: The Algorithm ML (<a href="https://github.com/twitter/the-algorithm-ml"target="_blank" rel="noopener">https://github.com/twitter/the-algorithm-ml</a>)</li>
<li>Twitter&rsquo;s Recommendation Algorithm (<a href="https://blog.twitter.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm"target="_blank" rel="noopener">https://blog.twitter.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm</a>)</li>
<li>Steven Tey: # How the Twitter Algorithm works in 2023 (<a href="https://steventey.com/blog/twitter-algorithm"target="_blank" rel="noopener">https://steventey.com/blog/twitter-algorithm</a>)</li>
<li>Aakash Gupta: The Real Twitter Files: The Algorithm (<a href="https://aakashgupta.substack.com/p/the-real-twitter-files-the-algorithm"target="_blank" rel="noopener">https://aakashgupta.substack.com/p/the-real-twitter-files-the-algorithm</a>)</li>
<li>Earlybird: Real-Time Search at Twitter (<a href="https://stephenholiday.com/notes/earlybird/"target="_blank" rel="noopener">https://stephenholiday.com/notes/earlybird/</a>)</li>
<li>Twitter Search is Now 3x Faster (<a href="https://blog.twitter.com/engineering/en_us/a/2011/twitter-search-is-now-3x-faster"target="_blank" rel="noopener">https://blog.twitter.com/engineering/en_us/a/2011/twitter-search-is-now-3x-faster</a>)</li>
<li>Paper: GraphJet: Real-Time Content Recommendations at Twitter (<a href="https://www.vldb.org/pvldb/vol9/p1281-sharma.pdf"target="_blank" rel="noopener">https://www.vldb.org/pvldb/vol9/p1281-sharma.pdf</a>)</li>
<li>Paper: RealGraph: User Interaction Prediction at Twitter (<a href="https://www.ueo-workshop.com/wp-content/uploads/2014/04/sig-alternate.pdf"target="_blank" rel="noopener">https://www.ueo-workshop.com/wp-content/uploads/2014/04/sig-alternate.pdf</a>)</li>
<li>Paper: Real-Time Twitter Recommendation: Online Motif Detection in Large Dynamic Graphs (<a href="http://www.vldb.org/pvldb/vol7/p1379-lin.pdf"target="_blank" rel="noopener">http://www.vldb.org/pvldb/vol7/p1379-lin.pdf</a>)</li>
<li>Paper: Discovering Similar Users on Twitter (<a href="http://snap.stanford.edu/mlg2013/submissions/mlg2013_submission_20.pdf"target="_blank" rel="noopener">http://snap.stanford.edu/mlg2013/submissions/mlg2013_submission_20.pdf</a>)</li>
<li>GitHub: FlockDB (<a href="https://github.com/twitter-archive/flockdb"target="_blank" rel="noopener">https://github.com/twitter-archive/flockdb</a>)</li>
<li>GitHub: Cassovary (<a href="https://github.com/twitter/cassovary"target="_blank" rel="noopener">https://github.com/twitter/cassovary</a>)</li>
<li>GitHub: GraphJet (<a href="https://github.com/twitter/GraphJet"target="_blank" rel="noopener">https://github.com/twitter/GraphJet</a>)</li>
<li>Twitter: Finagle (<a href="https://twitter.github.io/finagle/"target="_blank" rel="noopener">https://twitter.github.io/finagle/</a>)</li>
<li>Apache Avro (<a href="https://avro.apache.org/"target="_blank" rel="noopener">https://avro.apache.org/</a>)</li>
<li>AkitaOnRails: Chatting with Blaine Cook (Twitter) (<a href="https://www.akitaonrails.com/2008/6/17/chatting-with-blaine-cook-twitter"target="_blank" rel="noopener">https://www.akitaonrails.com/2008/6/17/chatting-with-blaine-cook-twitter</a>)</li>
<li>Fireship: Vector databases are so hot right now. WTF are they? (<a href="https://www.youtube.com/watch?v=klTvEwg3oJ4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=klTvEwg3oJ4</a>)</li>
<li>Slideshare: Restrição == Inovação (<a href="https://www.slideshare.net/akitaonrails/devconf-2019-so-carlos"target="_blank" rel="noopener">https://www.slideshare.net/akitaonrails/devconf-2019-so-carlos</a>)</li>
<li>Elastic: Vector Search (<a href="https://www.elastic.co/what-is/vector-search"target="_blank" rel="noopener">https://www.elastic.co/what-is/vector-search</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Já não é notícia nova, a esta altura acho que todo mundo já viu ou ouviu alguém comentando sobre o código dos algoritmos de recomendação do Twitter, que tio Elon Musk liberou, parcialmente, no último dia de março de 2023. No momento em que estava escrevendo o script deste episódio não se passou uma semana ainda. E até a gravação deste video ainda não foi liberada a segunda parte do código. Mas vamos ver o que temos até agora.</p>
<p>(&hellip;)</p>
<p>No mesmo dia que a primeira parte do código foi liberado, eu e diversos outros programadores e pesquisadores começamos a avaliar o código e surgiu muita coisa interessante. Eu mesmo fui twittando em tempo real o que achei de primeiras impressões. Meu foco foi mais na parte técnica e arquitetura. Os links pros meus tweets originais estão na descrição abaixo e neste video vou elaborar em mais detalhes o que não coube em pequenos tweets.</p>
<p>Antes de começar, preciso deixar alguns avisos muito claros aqui. Este código é incompleto. Ainda falta pelo menos uma segunda parte. E mesmo com essa parte, ainda não vamos ter certeza que tudo vai ser liberado. O que o Twitter soltou não é mais que uma pequena fração de todo o código necessário pra se fazer um verdadeiro clone de Twitter.</p>
<p>Esse código só cobre uma parte dos sistemas que devolvem os tweets recomendados que você vê na aba de &ldquo;Para você&rdquo;. Em termos coloquiais chamamos isso tudo de &ldquo;Algoritmo do Twitter&rdquo;, mas não é um único algoritmo: é uma coleção de dezenas de algoritmos e heurísticas que, funcionando juntos, geram recomendações que tem relevância pros seus interesses.</p>
<p>Mesmo sendo incompleto, estamos falando de mais de 4 mil e seissentos arquivos. Mais de 330 mil linhas de códigos, sendo mais da metade código feito na linguagem Scala, e quase 30% em Java. A próxima linguagem mais relevante depois dessas duas é Rust, na nova parte de machine learning chamada Navi, mas mesmo assim é só 2% do total desse código.</p>
<p>Mais importante, o sistema de build que eles utilizam é o Bazel. Pense NPM se você for de Javascript, ou algo parecido com Gradle se for de Java mesmo. Eu mesmo nunca vi Bazel sendo utilizado num projeto de verdade e cada sub-projeto tem um arquivo de BUILD em Bazel, mas eles omitiram os arquivos WORKSPACE, que são necessários pra conseguirmos realmente compilar e buildar cada projeto.</p>
<p>É possível reconstruir do zero, via tentativa e erro, mas é um trabalhinho bem chato que eu não tenho interesse em fazer e, até o momento, não vi ninguém se prontificando a fazer. Portanto tudo que eu, ou qualquer outro analisar, é baseado unicamente em análise estática do código, que é um jeito mais bonito de dizer que só temos como ler o código ou fazer compilações parciais. Por isso não acredite em tudo que ler em somente um pedaço do código. Deixa eu dar um exemplo.</p>
<p>Alguns pesquisadores foram mais interessados na parte das regras de negócio: os critérios que afetam o alcance de tweets. Em resumo, não teve nenhuma grande novidade bombástica além do que todo mundo mais ou menos já sabia só por observação. Vamos ver dois exemplos, o blog post do desenvolvedor Steven, da Vercel, e o blog post do Aakash Gupta. Ambos os links, novamente, estão na descrição abaixo.</p>
<p>No post do Steven, você vai ler que o algoritmo de ranqueamento vai determinar pesos em algumas ações em cima do seu tweet. Por exemplo, um like tem peso 0.5, um retweet é mais importante, tem peso 1, se alguém clicar no tweet, der reply ou like ou ficar mais que 2 minutos lendo, vai ter peso 11 e assim por diante. Mas, de onde ele tirou essa informação?</p>
<p>Ele coloca o link pro README no diretório projects/home/recap dentro do repositório the-algorithm-ml, que é o repositório menor de machine learning. A contagem de Scala e Java que falei acima foi no repositório principal the-algorithm, que é o maior; mas tem esse outro menor que é basicamente um projetinho Python, com 91 arquivos e umas 6 mil linhas de código pra PyTorch, que eles chamam de &ldquo;heavy ranker&rdquo;.</p>
<p>O problema é que o único lugar nesse código onde esses números de peso aparecem, é na documentação desse README. Olha só, se eu abrir o VS Code no projeto the-algorithm-ml e procurar por esse parâmetro &ldquo;is_negative_feedback_v2&rdquo;, que tem um peso negativo 74, só aparece no README, em nenhum lugar no código tem de fato esse valor.</p>
<p>Existe um arquivo &ldquo;local_prod.yaml&rdquo; que parece que configura esses parâmetros pra produção, e no servidor deles pode ser que esteja mesmo este número, mas no arquivo neste repositório, não tem nada. Portanto, não posso dizer que está nem certo, nem errado, só que é inconclusivo. Só que se você ler o post, vai achar que isso é um fato. E tenho certeza que um monte de gente vai referenciar esse blog post e repetir a informação sem checar o que significa. O Steven não errou nem mentiu, mas ele está descrevendo informação incompleta.</p>
<p>Mas não é só isso. Se for agora no post do tal Gupta, ele vai falar de outros parâmetros de peso. Por exemplo, ele fala que retweets dão um boost de 20 vezes. E de onde ele tira isso? É do repositório principal the-algorithm, no arquivo EarlybirdTensorflowBasedSimilarityEngine.scala no projeto cr_mixer. Lá tem esse trecho da função <code>getLinearRankingParams</code>. Ah, agora não é um README, é trecho de código. Então deve ser confiável, certo? Também não.</p>
<p>Vamos procurar quem chama essa função. Caímos nesse trecho da função <code>getRelevanceOptions</code> que, caso essa variável <code>useTensorflowRanking</code> seja falsa, daí vai chamar a função com os parâmetros que vimos antes, caso contrário, vai chamar essa outra função <code>getTensorflowBasedRankingParams</code>. Vamos ver o que ela define. Voltamos pro arquivo anterior e vemos que ela chama a função <code>getLinearRankingParams</code> abaixo, mas muda o tipo e, mais importante tem esse <code>applyBoosts</code> pra false.</p>
<p>O que isso significa? Será que então todos esses parâmetros com nome &ldquo;boost&rdquo; aqui embaixo vão ser ignorados? Como <code>offensiveBoost</code> ou <code>tweetHasTrendBoost</code>? Aí que tá, não temos como saber. Como disse antes, esse código é incompleto. Até o presente momento não sabemos nem se 100% desse código compila, não sabemos quais dependências estão faltando, não sabemos como buildar e, portanto, não sabemos como executar esse código e nem quais caminhos dentro são realmente executados pra cada tipo de dados e configurações.</p>
<p>E mesmo se conseguirmos fazer rodar, vamos voltar praquele README que mostrei agora pouco, da parte de machine learning com PyTorch. Olha o que diz: &ldquo;não podemos liberar os dados reais de treino por causa de privacidade&rdquo;. Faz sentido, claro, mas sem ter o mesmo material de treino, ou pelo menos o mesmo modelo gerado no final, não temos como reproduzir exatamente o que roda no Twitter e, portanto, não podemos garantir que é exatamente a mesma coisa rodando em produção nos servidores deles.</p>
<p>Portanto, do ponto de vista de auditoria, podemos confiar que, sim, esse código é certamente retirado do código real dentro do Twitter. É código demais pra falsificar, não seria prático. Mas os detalhes do que exatamente roda lá dentro, talvez a gente nunca saiba. Sempre é possível omitir camadas de filtros na fase de mixagem que adicionam ou removem tweets, ou omitir os arquivos de configuração reais que modificam todos esses pesos e boosts.  Não temos como realmente saber, por isso cuidado com afirmações sendo feitas só lendo esse código.</p>
<p>Antes de continuar, eu preciso falar da vergonha alheia que foi o comportamento geral das pessoas frente a esse código. Vamos estabelecer: eu não sou fã de Elon Musk, Dogecoin é shitcoin, e acho uma puta perda de tempo gente que idolatra o cara ou que perde tempo engajando. Ele é só mais um cara que existe. A existência dele não me afeta, assim como a existência de qualquer outro, pra mim, é insignificante. E é só isso.</p>
<p>O Twitter é uma empresa privada. Como qualquer empresa, ela não tem nenhuma obrigação de liberar código. Portanto, o que ela fez ao liberar, pra mim, é um excelente marketing. Uma cortina de fumaça pra desviar atenção, controlar a narrativa e deixar todo mundo coçando a cabeça tentando desvendar esse quebra cabeça; e ao mesmo tempo cutucar os críticos no governo e cutucar os concorrentes como um Zuckerberg da vida. No mínimo vai deixar muita gente chata ocupada tentando abrir uma caixa semi-transparente que não tem chave pra tranca.</p>
<p>A real motivação por trás disso não me interessa. Só me interessa que temos esse código. O que foi liberado, continua sendo um código excelente de explorar. Porém, pessoas idiotas parece que tem talento de encontrar a parte mais insignificante, e viralizar os memes mais idiotas. Se você pesquisar a palavra &ldquo;Elon&rdquo; no VS Code, a primeira coisa que vai aparecer é essa chave &ldquo;author_is_elon&rdquo;, logo em cima de outras chaves &ldquo;suspeitas&rdquo; como &ldquo;author_is_democrat&rdquo; ou &ldquo;author_is_republican&rdquo;. Uau, que vazamento inesperado! O que é isso, a prova que o Elon dá boost nos tweets dele? O que é isso de democratas e republicanos, tem manipulação neles também?</p>
<p>Até gente que se auto-denomina &ldquo;programador&rdquo; ficou fazendo meme disso. É uma vergonha alheia. Se você viu alguém fazendo meme disso, pode des-seguir, o cara é inútil. Deixa eu mostrar como faz. Vamos lá pro código. Esse &ldquo;author_is_elon&rdquo; é chave pra um Map que no final vai se chamar PredicateMap, que é gerado através desta Sequence, onde cada elemento é um tuple com uma string que serve de chave e um FeatureMap, que é uma função booleana. No caso desse trecho do Elon, ele recebe um candidato, provavelmente candidato de tweet pra recomendar, e checa se a tal feature de Author contém esse tal DDGStatsElonFeature. Ênfase em &ldquo;stats&rdquo;.</p>
<p>Bacana, e quem usa esse PredicateMap? Vamos procurar no código. Achamos aqui na linha 82 do objeto ServedEventsBuilder que extende essa trait ClientEventsBuilder. Nesse trecho ele tá iterando por cada predicado naquela PredicateMap e no final o que  tá gerando são counts pra cada predicado. É um contador. E quem vai usar essas contagens? Vamos de novo pesquisar, agora por esse objeto <code>ServedEventsBuilder</code>.</p>
<p>Pronto, chegamos neste <code>HomeScribeClientEventSideEffect</code>. É o que o Twitter chama de &ldquo;side-effect&rdquo;. Pelo que eu já tinha deduzido lendo o código, side-effect é um job de Kafka, um job assíncrono. E Scribe é justamente o servidor de logs open source que foi desenvolvido pelo Facebook. É só um servidor de log, não diferente de um Apache Flume ou Logstash, ou Rsyslog da vida. Esses códigos servem pra gerar métricas. É só isso.</p>
<p>&ldquo;Ah, mas tinha que ser o cuzão egocêntrico do Elon pra ter um código especial pra uma métrica só pra ele.&rdquo; Ué, mesmo se for. Se ele desenbolsou 40 bilhões pra comprar a porra toda, achei foi muito pouco código com o nome dele. Achei que fosse achar classes inteiras renomeadas pra DogeCoin ou algo assim espalhado por aí. Aliás, vocês estão assumindo que esse código foi feito por ordem do Elon.</p>
<p>Outra teoria muito mais plausível é que esse código foi feito justamente pelo povo que ele demitiu, pra monitorar os efeitos dos tweets do Elon antes da compra ter sido realizada. Muito mais plausível. Independente do que for, só vamos saber se, alguém de dentro do twitter, vazar os logs do repositório Git de verdade, com o timestamp do commit. E mesmo assim, commits de Git podem ser facilmente adulterados. Por isso é uma discussão meio inútil.</p>
<p>Todo mundo ficou fazendo meme com um código de log. E vocês se acham programadores? A discussão do porque esse trecho existe é irrelevante e só serve pra demonstrar como é um programador que gosta de fazer bikeshed: discutir coisas inúteis, sem nenhum valor, sem nenhum mérito. Você está perdendo seu tempo. Agora vamos entender como um programador de verdade olha pra esse código.</p>
<p>O objetivo a partir daqui é o seguinte: desmistificar a diferença de código de curso, código de tutorial e código de produção de empresa de verdade. Pra um iniciante que nunca trabalhou ou só trabalhou em projetos pequenos, a diferença vai ser gritante. A outra coisa é como um programador sênior pensa. Infelizmente, não tem como, num único video, com só 2 dias de análise, sem compilar, conseguir ter a visão completa. Então tudo que vou dizer são só minhas primeiras impressões.</p>
<p>Eu já disse isso em outros videos, mas aqui vale repetir: código de curso, código de tutorial, código em video no Youtube, são absurdamente diferentes de código de verdade de produção. Não tem nem comparação. A complexidade e tamanhos são impossíveis de caber em qualquer curso ou tutorial. É inviável. Literalmente, sem exagero, um curso vai te ensinar a fazer uma casinha de rato com caixa de sapato, mas todo projeto relevante de verdade é um Burj Khalifa, o arranha-céus mais alto do mundo que nunca foi construído, e toda a engenharia pra construir isso num terreno de areia, sem receita pré-pronta.</p>
<p>Outra coisa que já disse antes: software nunca acaba. Se seu produto realmente vale alguma coisa, você vai reescrever partes ou até ele inteiro, mais cedo ou mais tarde. E com esse código liberado pelo Twitter, vou finalmente poder ilustrar esses dois pontos. E esse vai ser o objetivo do video.</p>
<p>Eu peguei aleatoriamente o primeiro projeto no Github com nome de &ldquo;twitter clone&rdquo;. Tem dezenas por aí. Esse é feito em Next.js, com Typescript, Firebase, Tailwind, só &ldquo;tecnologia de ponta&rdquo;, como um amador poderia pensar. E ele faz tudo, olha só o Readme: usuários conseguem postar tweets, dar like, retweet, reply, adicionar bookmarks, adicionar imagens, seguir ou des-seguir outros usuários, updates de likes e tudo mais em tempo real. Porra, faz tudo.</p>
<p>Só que não. Isso não passa de uma caixa de sapatos frente ao Burj Khalifa. Vamos ver, com a ferramenta Tokei, vemos que esse projeto tem 158 arquivos e mais de 32 mil linhas de código. Pra um iniciante, sem dúvida, pode parecer gigantesco. Mas não é. Mesmo sendo só uma fração incompleta, o repositório do Twitter que estamos avaliando, tem mais de 4600 arquivos e mais de 330 mil linhas de código. 10 vezes mais código.</p>
<p>Mas esses projetos sequer são equivalentes. Esse tal &ldquo;clone&rdquo; é só o front-end e só as operações mais básicas. Ele jamais aguentaria a escala do Twitter em produção. O &ldquo;the-algorithm&rdquo; que foi liberado é só pra buscar os tweets que aparecem na aba &ldquo;Pra você&rdquo;. Notaram como esses clones omitem a aba &ldquo;pra você&rdquo;? Claro, porque justamente a inteligência pra buscar tweets recomendados, em tempo real, em escala, não é um problema trivial de resolver. Todo clone que você achar, ou vai omitir, ou vai ter uma versão super besta de recomendação. E nunca vão escalar pra ordem de centenas de milhões de pessoas. São todos, caixas de sapato.</p>
<p>E não tem nenhum problema nisso. O foco da maioria desses clones é ser um exercício pra iniciantes de front-end treinarem fazer interfaces parecidas com coisas que já existem. Nada mais, nada menos. Nenhum desses clones, seja clone de Twitter, clone de Instagram, clone de Pinterest, chega perto da unha dos pés das versões de verdade e são incapazes de fazer uma fração do que os originais fazem, ou de aguentar uma fração do tráfego. Essa é a realidade dos clones: são todos meros exercícios. Contanto que vocês entendam isso, não tem nenhum problema se exercitar neles. Todo mundo precisa começar do zero. E esse é o nível zero.</p>
<p>A razão dessa diferença é que é trivial fazer qualquer aplicativo funcionar só na sua máquina, com uma pessoa usando. Mesmo se você virar freelancer, normalmente trabalha em projetos que muito pouca gente usa. 10 pessoas. 100 pessoas. Mesmo mil pessoas, é extremamente pequeno. Empresas grandes como MercadoLivre, iFood, Nubank, lidam com milhões, dezenas de milhões de pessoas. No caso de Facebook, Google ou Apple, são bilhões de pessoas.</p>
<p>E nessa escala, as soluções são completamente diferentes de  todo curso e tutorial que você procurar. Eu afirmo que não existe nenhum curso que cobre casos de larga escala, simplesmente porque todos fazendo cursos nunca tiveram que lidar com esse problema. Eles também simplesmente não sabem que isso existe. Eu mesmo nunca tive que lidar com o caso excepcional de bilhões de pessoas.</p>
<p>É muito caso a caso, em cada empresa gigante, vai ser diferente. Nenhum curso conseguiria cobrir, mesmo se quisessem. Simplesmente não existe esse material disponível, e é uma das razões de porquê esse código do Twitter é relevante: porque não existe código de produção de empresa de verdade disponível pra podermos mostrar, todos são fechados.</p>
<p>Eu trabalhei em vários projetos, incluindo alguns dessa categoria grande. Mas não posso mostrar, porque nenhum de nós deve quebrar confidencialidade e propriedade intelectual de clientes. Por isso eu falo pra não confiar em nenhum programador que não tenha tido esse nível de experiência: eles nunca viram nada além da dimensão de caixa de sapato e acha que o resto do mundo é pequeno assim. E definitivamente não é. Só quem já subiu no topo do Burj Khalifa, sabe a diferença.</p>
<p>A tal aba &ldquo;Pra você&rdquo;, à primeira vista, parece super simples. Na aba &ldquo;seguindo&rdquo; mostra só tweets de pessoas que eu sigo, provavelmente em ordem cronológica. Fácil de fazer, um iniciante consegue imaginar um &ldquo;select top 100 * from tweets order by created_at desc&rdquo; da vida. Agora, no pra você, tem tweets de gente que eu sigo e de gente que eu não sigo, mas que as pessoas que eu sigo interagiram. Então se eu sigo o João e o João retwitou o tweet do Paulo, no meu &ldquo;pra você&rdquo; pode aparecer o tweet do Paulo. Só que não é tão simples assim.</p>
<p>E se quem eu sigo interagiu com 100 tweets? Quais deles deveriam aparecer pra mim? Das pessoas que eu sigo, quais delas são mais relevantes pra aparecer no topo primeiro? Por que eu vejo o like de algumas pessoas mas não de outras? Ah, será que como os posts de blogs tavam tentando explicar, é só botar um peso pra like, outro peso pra retweet e dar um &ldquo;group by&rdquo; ou &ldquo;order by&rdquo; da vida e pegar o topo? Também não.</p>
<p>Como é impossível cobrir o código todo que foi liberado, escolhi começar falando só de um pequeno trecho. Um banco de dados NoSQL de grafos, que inclusive já tinha sido liberado antes como open source, com licença Apache, chamado GraphJet, e a história por trás dele. Tudo isso foi descrito num paper de autoria de Aneesh Sharma e vários outros colaboradores, publicado em 2016.</p>
<p>Pra todo mundo que não fez ciências da computação entender, na faculdade a gente aprende sobre um troço chamado &ldquo;grafos&rdquo;. Na prática, você já deve ter visto e até desenhado. É quando se desenha caixinhas ou círculos que chamamos de nós e ligamos um nó com outro nó com um troço que chamamos de vértices. Podemos representar grafos como matrizes adjacentes ou listas adjacentes. Outra forma de representar grafos é com vetores.</p>
<p>Daí podemos classificar grafos baseados em características como número de nós ou número de vértices, edges ou bordas, direção das bordas. Tem diversos tipos de grafos como unidirecionais. Em Git, por exemplo, você já deve ter ouvido falar que é um DAG, um direct acyclic graph, ou grafo acíclico, que não é cíclico, e direto.</p>
<p>Dado um grafo, temos como navegar de um nó pra outro nó. Pense em nós como uma casa numa rua e os vértices como o caminho, talvez a rua, entre essas casas. Daí temos algoritmos pra navegar como o algoritmo do menor caminho entre dois nós, que é o hello world de grafos. Não só isso, ainda no assunto de classificação, temos nós que ficam nas bordas do grafo que tem poucos vértices e temos nós no meio dos grafos com muitos vértices.</p>
<p>Costumamos chamar esses nós de &ldquo;hubs&rdquo;. Na prática, numa rede social, chamamos de hubs, mas pessoas comuns chamam de &ldquo;influencers&rdquo;. Pessoas comuns são os nós das bordas, com poucos relacionamentos, que são os vértices.</p>
<p>Tudo na natureza pode ser representado como grafos. Como falei antes, evolução de desenvolvimento de software, pode ser representado como um DAG, um grafo de commits, com branches. Um sistema de logística e entregas é orientado num mapa com grafos, pra determinar o melhor caminho de entrega entre dois pontos. O DNA humano pode ser um grafo, onde os nós são as bases como adenina, citosina, guanina e timina, formando nucleobases. As sinapses do nosso cérebro são grafos.</p>
<p>Num ecossistema animal temos a hierarquia da cadeia alimentar, que pode ser representado como um grafo. No mercado temos grafos de empresas e relacionamentos de comprador e vendedor. E assim por diante. No caso do Twitter, que é uma rede social, os nós costumam ser as pessoas, e os vértices são os relacionamentos, quem nós seguimos, quem damos likes ou retweets, quem bloqueamos e assim por diante.</p>
<p>Se você nunca estudou ciências da computação, vai ter um pouco de dificuldade de entender o que vou explicar e certamente não vai entender nada do código do Twitter. Vou tentar simplificar o máximo que der, mas só consigo chegar até certo ponto, então, se interesse em depois ir procurar cursos como nas aulas do MIT sobre grafos. Pra operar em grafos, você vai precisar aprender coisas como Álgebra Linear e Cálculo, pelo menos o Cálculo básico.</p>
<p>E portanto sim, pra entender como uma rede social funciona e o código por trás, precisa entender esses aspectos da matemática. E pra chegar no ponto de recomendação, também precisa entender o básico de estatística e probabilidade, porque recomendação é justamente a probabilidade dos tweets que o sistema recomendar realmente te interessar a ponto de engajar, dar like ou reply, que é a base de machine learning.</p>
<p>Mesmo se você não for implementar do zero inteligência artificial, machine learning, ainda assim precisa entender essa matemática. Machine Learning é, nada mais, nada menos, do que uma coleção de algoritmos de classificação e probabilidade. Se não souber quando pode usar regressão linear, regressão logística, similaridade de cosseno, bayes ingênuo, não vai saber o que fazer.</p>
<p>Cada caso, cada tipo de conjunto de dados e o resultado que quer, depende de saber escolher quais desses algoritmos usar. E pra saber quais usar, precisa saber a matemática por trás. Senão vai usar regressão linear quando não podia e ter resultados errados, que é a coisa mais comum de ver amador fazer. Nem tudo que parece uma reta, é uma reta.</p>
<p>E no caso desse código do Twitter sim, eu encontrei que ele usa coisas como grafos, regressão logística e similaridade de cosseno. Mas estou me adiantando. Vamos voltar ao tal paper do Graphjet de 2016. Um problema, que não é trivial de resolver e não tem uma solução única, porque depende do comportamento dos usuários e dos dados disponíveis, é recomendação. Seja recomendação de produtos num ecommerce, recomendação de leitura num site de notícias, ou recomendação de tweets num Twitter.</p>
<p>Como usuários seguem usuários, naturalmente temos grafos, em particular grafos bipartidos, com usuários de um lado e tweets de outro. Os esforços de recomendação começaram no Twitter por volta de 2010, lembrando que o Twitter começou a operar em março de 2006.</p>
<p>Eu mesmo criei minha conta só um ano depois, em abril de 2007, durante a Railsconf 2008 em Portland, onde eu via todo mundo se cadastrando pra gente compartilhar onde ia ser o próximo happy hour. Lembrando que ainda não existia Messenger do Facebook, não existia Whatsapp, não existia Telegram. O único aplicativo de mensagem que todo mundo usava, era SMS.</p>
<p>De qualquer forma, quando eles começaram a experimentar a recomendar usuários novos pra gente seguir, por volta de 2010, foi com um sistema que chamaram carinhosamente de &ldquo;WTF&rdquo;, What the Fuck. Mentira, era &ldquo;Who To Follow&rdquo;. A arquitetura era como nessa figura. Segundo o paper, no seu núcleo tinha esse Cassovary, que é um motor de processamento de grafos in-memory, ou seja, que mantém os grafos em memória. Isso é importante, porque a maioria dos algoritmos conhecidos pra lidar com grafos assume ter o grafo todo em memória. Guarde essa informação.</p>
<p>Esse projeto Cassovary é open source e podemos encontrar no GitHub deles. Última atualização foi de 2021, e não é mais utilizado hoje. Continuando, esse Cassovary operava em snapshots de grafos de seguidores carregados do HDFS, que é o sistema de arquivos distribuídos do Hadoop. Quem já trabalhou com Big Data, Data Lakes, certamente já esbarrou em instalações de HDFS e no Twitter não era diferente. Só entenda que é um sistema de arquivos distribuído, como se você tivesse uma partição única espalhada em múltiplos servidores. É um nível acima de um simples RAID.</p>
<p>Os grafos de seguidores vinham de outro banco de dados chamado FlockDB que fazia a ingestão desses dados no HDFS. FlockDB acho que foi o primeiro banco de dados de grafos que o Twitter criou e soltou como open source. O que aconteceu foi: eles começaram como qualquer tech startup normal, sem saber o que o produto ia se tornar. No começo usavam um banco de dados normal, MySQL mesmo. Mas quando começaram a fazer sucesso e ver que o problema era maior que um banco relacional, começaram a pesquisar alternativas. E o primeiro resultado foi um banco de dados de grafos, o FlockDB.</p>
<p>O FlockDB dava mais performance pra operações de inserção, updates, já que não precisava das garantias ACID de um banco relacional. Tinha operações extras pra lidar com grafos, como navegar por nós e pelos vértices. E vocês tem que entender que carregar um grafo em memória pode ser pesado. Imagine sua própria conta: você tem dezenas de tweets, você segue dezenas de pessoas, essas pessoas tem dezenas de tweets.</p>
<p>Cada um desses tweets tem contagens de likes, retweets; alguns tweets são replies pra outros tweets. E pra calcular qual deles é mais relevante ou menos relevante, o ideal é ter toda essa estrutura em memória pra conseguir navegar por toda essa informação. Agora pensa num banco de dados relacional como MySQL. Como você resolveria esse problema? Qualquer solução que você imaginar em SQL, pode ter certeza, já foi testado e sabemos que não vai escalar. Essa é um dos poucos cenários que podemos afirmar que um banco de dados relacional não é adequado.</p>
<p>Segundo a página de GitHub do FlockDB, até 2010 o cluster deles armazenava mais de 13 bilhões de bordas e sustentava picos de tráfego de mais de 20 mil escritas por segundo, e mais de 100 mil operações de leitura por segundo. Mas rapidamente chegou um ponto onde isso já não era mais suficiente. Foi quando começaram a puxar esses dados pra HDFS e processando nesse novo projeto que chamaram de Cassovary, um segundo projeto em Java pra lidar com &ldquo;Big Graphs&rdquo;, grafos com bilhões de nós e vértices. Foi a época que as celebridades gigantes começaram a viver no Twitter. Pense nível Kardashians.</p>
<p>Voltando ao paper, a camada de armazenamento do Cassovary dava acesso aos grafos via queries, pesquisas baseadas em vértices. Pense um &ldquo;SQL&rdquo; especializado em grafos. E em cima disso um motor de recomendação computava as sugestões de &ldquo;Who To Follow&rdquo;, o WTF, ou &ldquo;quem seguir&rdquo;. Foi assim que nos primórdios do Twitter a ganhamos sugestões de novas contas pra poder seguir.</p>
<p>Um dos objetivos desse projeto era ver se era possível manter os grafos em memória, in-memory. A pergunta que eles não tinham resposta é: será que o crescimento do volume de dados desses grafos seria mais lento que a Lei de Moore? Se for, o preço de memória iria baratear mais rápido do que os dados cresciam, e daria pra computar tudo em memória. Que é mais simples do que ter que lidar com discos, particionamento, paging, cache e tudo mais.</p>
<p>Como o paper diz, considere um grafo com 10 bilhões de bordas: mesmo uma representação ingênua, simples, ocuparia algo na faixa de 80 gigabytes, que um único servidor consegue aguentar. Você já trabalhou com um servidor, este ano, que tem 80 gigabytes? Se sim, diga nos comentários que empresa é, pra todo mundo entender meu ponto.</p>
<p>Uma das limitações, mesmo naquela época, é que essa sugestões de recomendação de quem seguir, eram calculados em batch, uma vez por dia, não era em tempo real como é hoje. Ou seja, você só tinha recomendações novas uma vez por dia. De qualquer forma, via muita experimentação, muitos testes A/B, eles chegaram em dois conceitos que usam até hoje e que vale explicar. O primeiro se chama círculo de confiança e o segundo foi a adoção de &ldquo;SALSA&rdquo; que é Stochastic Approach for Link-Structure Analysis ou Análise de estrutura de links usando processos estocásticos.</p>
<p>Um exemplo de grafo que eu não mencionei, mas deveria ser óbvio, é a própria World Wide Web. Toda página na internet é um nó, as bordas, vértices, são links de uma página pra outra. No começo da web, já se tinha um número grande de páginas, o suficiente pra ser difícil de achar as coisas. Pense numa era antes de existir Google. A forma rudimentar que existia pra gente se achar era derivado do conceito antiquado de Páginas Amarelas.</p>
<p>Se você for muito novo talvez nunca tenha visto, mas antigamente, quando precisávamos achar um mecânico, um médico ou qualquer serviço, tínhamos um livrão físico de páginas amarelas que era um diretório com todos os serviços da cidade. Cada cidade tinha um livro diferente. Imaginem que novos negócios tinham dificuldade de serem encontrados, porque precisava esperar uma nova edição ser impressa e distribuída pra todo mundo. Páginas de classificados nos jornais meio que cobriam um pouco dessa diferença.</p>
<p>Serviços como Yahoo, Excite, Lycos e outros serviam esse papel na Web. Se eu criasse um site novo, pedia pra ser incluído num desses diretórios pra que as pessoas conseguissem me achar. Esse era o processo. Mas mesmo assim, imagine restaurantes. Qual das centenas de restaurantes numa cidade grande como São Paulo, seria relevante pra mim? Mesmo se tivesse a opção de filtrar por localização e tipo de cozinha. Ainda assim eu tinha que manualmente tentar várias. Era bem trabalhoso.</p>
<p>Portanto, resolver esse problema de relevância entre milhares de páginas web, era muito importante. E lá no meio pro fim dos anos 90 surgiram pelo menos dois estilos diferentes de lidar com isso. A maioria de vocês que estudou o mínimo de grafos talvez estejam familiarizado com o famoso paper de PageRank, literalmente ranking de páginas, inventado pelos fundadores do Google, Larry Page e Sergey Brin em 1996 quando ainda eram estudantes de Stanford.</p>
<p>Procurem no YouTube, tem dezenas de videos que explicam PageRank, mas a idéia básica é a seguinte. Começa pelo básico, o &ldquo;H&rdquo; de HTML é pra Hyperlink. Todo mundo esquece isso, mas o mais importante numa página web é que a página de alguém contenha um link pra sua página. Quanto mais sites diferentes tem links apontando pro seu site, mais relevante ele pode ser.</p>
<p>No PageRank, imaginamos um navegador aleatório, uma pessoa imaginária que vai clicando em páginas e nos links nessas páginas, aleatoriamente. Qual a probabilidade de, só navegando sem rumo, alguém cair na sua página? Esssa probabilidade é o seu score, sua pontuação. Se sua página é famosa, centenas de outras páginas contém link pra sua, a probabilidade desse navegador aleatório chegar na sua página é alta, portanto sua pontuação no ranking, no PageRank, é alta.</p>
<p>Esse foi o algoritmo usado na primeira versão do Google, o site que matou todos os diretórios estilo páginas amarelas. Porque agora era possível fazer um programa que sai vasculhando todas as páginas na web, avaliando os hyperlinks entre eles, e atribuindo pontuações nesse ranking. Daí, quando fazemos uma pesquisa, ele nos mostra primeiro as páginas mais bem pontuadas. É por isso que logo na primeira página, os primeiros links costumam ser os corretos.</p>
<p>Com isso eliminamos a necessidade de seres humanos ficarem manualmente classificando páginas em diretórios e atualizando a relevância na mão, que sempre foi demorado, trabalhoso, tedioso e com muita margem de erro. Essa talvez tenha sido a primeira categoria de trabalhadores da web que perderam seu trabalho pra um algoritmo, e não será a última. Tudo que é feito manualmente e pode ser automatizado, certamente será. Todo trabalho repetitivo é automatizável, sabemos isso desde o começo da Web.</p>
<p>Mas todo algoritmo tem seu ponto fraco e rapidamente as pessoas começaram a notar que se o lance é esse número de links pra sua página, que tal criar um monte de sites fantasma que fazem links pra uma página que você quer tornar relevante? E assim surgiram os primeiros web farms, fazendas de servidores fake, e uma galera que vendia relevância pro seu site, fazendo um monte de site falso linkar pra sua. Seja criando sites bestas em série ou invadindo sites dos outros pra colocar links.</p>
<p>Isso aconteceu só lá no começo dos anos 2000. Por isso o Google evoluiu e deixou de usar PageRank e passou a usar, por exemplo, TrustRank, que combate web spam identificando quem é site legítimo e quem não é, e descontando pontuação. Ao longo dos anos o Google sempre foi aprimorando esses algoritmos, não é um produto fixo. Parte do trabalho de agências web era ficar monitorando quando o Google lançava uma nova versão pra modificar as estratégias pra levar relevância pros seus clientes. Esses truques baratos não funcionam mais hoje.</p>
<p>Mas não era só isso. Mesmo sem spammer e gente maliciosa, as primeiras versões do PageRank ainda sofriam de um problema grave. Pra ilustrar, veja meu conteúdo, meu canal e meu blog, que é de um domínio muito específico de assuntos de tecnologia pra programadores. Talvez eu faça um video explicando em detalhes sobre o algoritmo do Twiter, exatamente o que estou fazendo agora. Pra programadores é relevante.</p>
<p>Porém, imagine que um bosta como um Felipe Neto também faça um video falando sobre o algoritmo do Twitter. Segundo o PageRank, que é um ranking global, quer você goste dele ou não, se pesquisar no Google de 1997 &ldquo;algoritmo do Twitter&rdquo;, esse video do Felipe Neto iria aparecer na frente do meu. Essa é a desvantagem de um algoritmo de ranking global como o PageRank original.</p>
<p>Mas como eu falei, as pesquisas em torno de grafos evoluíram rápido. Por exemplo, em 1999 surgiu o algoritmo HITS que significa Hyperlink-Induced Topic Search, de Jon Kleinberg. HITS opera num grafo direcionado, onde páginas web representam nós e hyperlinks são bordas direcionadas. Até aqui, igual PageRank, mas o HITS assinala duas pontuações pra cada página web: uma pontuação se é um &ldquo;hub&rdquo; e outra pontuação se é uma &ldquo;autoridade&rdquo;.</p>
<p>Hubs são páginas que linkam pra diversas outras páginas. Pense um site do Estadão ou da Folha. São hubs de notícias que linkam pra trocentas outras páginas fora. Ou influencers como o Nelipe Feto, é um hub. Já autoridades são páginas que recebem muitos links de muitos hubs. Pense a página da Receita Federal em época de imposto de renda. Todo mundo faz link pra ela, pra avisar que tá vencendo a data ou ensinando como preencher seu imposto. Mas todo mundo pode ser ao mesmo tempo um hub e uma autoridade, alguns são mais hubs, alguns são mais autoridades num domínio em específico. Por isso duas pontuações distintas.</p>
<p>Portanto, o canal de um Nelipe Feto da vida teria uma pontuação de Hub muito maior do que de autoridade, que é exatamente o perfil do influencer médio: serve pra direcionar tráfego pra todo mundo, mas ele mesmo não é autoridade de quase nada. Já caras como eu geram muito pouco tráfego pros outros, mas todo mundo me menciona e linka pra mim em assuntos de um domínio específico de programação e tecnologia.</p>
<p>Com isso, minha pontuação de autoridade, num video sobre &ldquo;algoritmo do Twitter&rdquo;, seria maior que do Nelipe e eu apareceria no topo da pesquisa, mesmo ele tendo muito mais tráfego e sendo muito mais famoso. Essa é a vantagem de algoritmos da família do HITS, se comparado ao PageRank original: ele torna esse jogo bem mais justo do que uma pontuação única global, e tenta melhorar a confusão entre influencer e autoridade. Só porque alguém tem muito seguidor, de jeito nenhum o torna autoridade de coisa nenhuma.</p>
<p>Porém, se antes, a forma ingênua era ter uma única pontuação global do PageRank, em HITS teríamos sub-grafos pro domínio ou tópico, por exemplo, programação, gastronomia, viagens, cinema e cada vértice teria duas pontuações, de hub ou autoridade, por tópico. Portanto dá pra imaginar que é mais caro computar HITS do que PageRank. Claro, o que temos hoje num Google é uma amálgama dessas técnicas e muitas outras que surgiram ao longo do tempo. O que nos leva de volta a SALSA.</p>
<p>SALSA poderia ser considerado uma evolução da idéia de HITS pra análise de links, mas uma das idéias é não precisar navegar por todos os vértices e pontuar o grafo inteiro. Em vez disso ele implementa a idéia de andarilho aleatório ou random walker. É que nem pesquisas de opinião pública. Todo mundo tá acostumado a ver agências de fake news, cof, quero dizer, imprensa tradicional, fazendo pesquisas de opinião na rua pra dizer &ldquo;ah, metade dos brasileiros considera o governo atual uma bosta&rdquo;. Mas como eles sabem disso? Eu não lembro de ninguém me perguntando isso.</p>
<p>Essa é a mágica da estatística. Podemos fazer sampling, ou seja, pesquisar só uma fração da população. Quanto mais aleatório for esse sampling, melhor. Um coisa é perguntar pra 100 pessoas só de São Paulo, outra coisa é perguntar pra 4 pessoas de cada estado. Assumindo que escolhemos bem esse grupo, dentro de uma margem de erro, podemos afirmar que a população em geral tem essa ou aquela opinião, economizando ter que perguntar pros mais de 200 milhões de cidadãos do país. É uma puta economia de 200 milhões pra 100. Entendem?</p>
<p>Mesma coisa num grafo. Eu não preciso necessariamente andar o grafo inteiro, posso escolher um grupo aleatoriamente, em vez de usar o modo iterativo original do HITS, economizando muito processamento e tendo resultados que caem em margens de erro estatisticamente aceitáveis. Pra otimizar mais isso, ele também reorganiza os nós e vértices num grafo bipartido, que ajuda a separar os papéis de hubs e autoridades.</p>
<p>Posso separar nossos nós em dois conjuntos distintos: um com páginas que linkam pra outras páginas, os hubs, e outro grupo com páginas que recebem links, as autoridades. Não vale a pena eu detalhar hoje, mas gravem esses dois conceitos de random walk e grafo bipartido pra estudar depois se tiverem interesse.</p>
<p>Expliquei tudo isso porque o paper do Twitter menciona que eles desenvolveram um algoritmo de recomendação baseado em SALSA, que é um algoritmo de random walk na mesma família de HITS, originalmente desenvolvido pra ranking de pesquisa web. O algoritmo constrói um grafo bipartido de uma coleção de websites de interesse (hubs de um lado e autoridades de outro), e cada passo no SALSA navega por duas bordas - um pra frente e outro pra trás. - agora você sabe o que esse parágrafo do paper está dizendo.</p>
<p>E ele continua dizendo: nós adaptamos SALSA pra recomendação de usuários da maneira mostrada nesta figura. O lado de hubs na esquerda é populado com o tal círculo de confiança do usuário, os usuários que ele escolheu manualmente seguir. As &ldquo;autoridades&rdquo; no lado direito são populados com usuários que esses hubs interagem. Depois que esse grafo bipartido é construído, rodamos múltiplas iterações de SALSA, que assinala pontuações.</p>
<p>Daí os vértices do lado direito são ranqueados por essa pontuação e tratados como recomendações de usuários. Os vértices do lado esquerdo também são ranqueados e interpretados como similaridade entre usuários. Baseado no princípio da homofilia, que é só um nome bonito pra dizer que pessoas com interesses parecidos tendem a se agrupar mais, podemos apresentar esse resultado como recomendações, que antigamente era uma seção chamada &ldquo;similares a você&rdquo;.</p>
<p>Resolvi dar uma detalhadinha a mais até aqui pra tentar explicar como foi o raciocínio por trás da primeira versão do motor de recomendação, mas agora vou tentar andar um pouco mais rápido, porque esse foi o sistema WTF que não existe mais. Sim, toda essa pesquisa, experimentação, durou pouco tempo. Apesar de ser um sistema bom, que funcionou bem, ele ainda não é o que conhecemos hoje. E em 2012 eles começaram a perseguir o tal projeto Cassovary que mencionei antes.</p>
<p>Pra ter recomendações ainda melhores, eles queriam usar mais sinais dos logs de comportamento dos usuários. Comportamento de likes, retweets, replies, follow e tudo mais. Mas isso torna os metadados do grafo ainda maiores e a premissa da primeira versão, que era tentar manter tudo em memória, já não cabia mais. Daí veio a decisão de tentar usar Hadoop, em particular Apache Pig que, pra quem não sabe, é tipo um compilador que produz sequências de programas MapReduce.</p>
<p>Hadoop é uma plataforma de tecnologias criadas pra lidar com o problema de Big Data no auge da Bolha da Internet no começo dos anos 2000. Pra muita gente ainda hoje, o conceito de Big Data não é muito palpável, porque ninguém lida com isso no dia a dia. Varia de produto pra produto, mas quando estamos gerando coisa como gigabytes por dia de dados, terabytes por mês, estamos em território de Big Data. O que você faz, com um banco de dados que tem centenas de terabytes? Deita e chora, porque não importa quão bom seja seus índices e queries, sua aplicação vai ficar absurdamente lenta, só pelo tamanho.</p>
<p>Nessa ordem de grandeza estamos falando que não é possível manter todos os dados numa única máquina, mesmo sendo um servidor grande, seja por limitação física ou de custo. Daí a única solução é a estratégia de Napoleão: particionar, dividir esse tanto de dados em múltiplos servidores, e daí conquistar. Por isso a base do Hadoop começa com o HDFS, que é o Hadoop File System, um sistema de arquivos distribuído.</p>
<p>Agora o problema é o seguinte: na faculdade você aprendeu a lidar com dados todos no mesmo lugar. Por exemplo, digamos que você tem uma lista de produtos e preços e queremos achar o produto mais barato, como faz? Simples, escolhe um algoritmo qualquer de ordenação como um Quicksort ou Mergesort da vida, e pega o primeiro elemento que vai ser o mais barato. Pra fazer isso, precisamos iterar elemento a elemento da lista pelo menos uma vez. Mas e agora, e se estamos com 5 servidores e essa lista foi dividida por 5?</p>
<p>Intuitivamente, poderíamos pensar, beleza, vou ter que repetir a ordenação uma vez em cada máquina. Daí no final vou ter uma lista parcial com os 5 produtos mais baratos achados em cada um dos 5 servidores, daí faço uma última ordenação nessa lista e finalmente acho o mais barato. Cada um dos servidores vai rodar em um quinto do tempo do que se fosse a lista completa, mas no final, ainda tem pelo menos mais uma operação de ordenação extra e aí tenho o resultado. Então seria tecnicamente um pouco mais &ldquo;lento&rdquo; rodar em vários servidores separados, mas vai ser menos caro.</p>
<p>Isso de rodar um algoritmo em pedaços dos dados, mapear os resultados parciais e depois reduzir na resposta final, é o que, a grosso modo, chamamos de MapReduce, que é parte fundamental de como fazer processamento de dados em Hadoop. Novamente, não vou detalhar MapReduce então anote pra pesquisar mais depois. MapReduce ficou famoso porque, depois do PageRank, é como o Google conseguiu operar em larga escala e deixou todo mundo no chinelo no começo dos anos 2000.</p>
<p>Hoje em dia é como toda grande empresa de tecnologia funciona, mas 20 anos atrás, essa forma de pensar em processamento de dados foi fundamental pra possibilitar termos produtos que aceitam quantidades massivas de pessoas e transações, literalmente ultrapassando a população total de diversos países. Tem mais gente no Twitter do que a população dos Estados Unidos, metade da população do mundo tá em produtos da Meta.</p>
<p>Por isso, pra segunda versão do motor de recomendação, o Twitter deixou o FlockDB e WTF pra trás e perseguiu o tal projeto Cassovary pra rodar em cima do HDFS do Hadoop, usando MapReduce. Mas eles tiveram vários problemas. Mesmo em 2012 já era sabido que grafos não performavam bem em Hadoop. Isso porque a maioria dos algoritmos conhecidos pra processar grafos são iterativos, lembra que falei pra guardar essa informação?</p>
<p>Pra ilustrar. Pense nos diferentes algoritmos de ordenação que aprendeu na faculdade. Alguns professores cometem o pecado de não explicar porque existem tantos e porque foram criados. Por exemplo, meu professor na faculdade mandou a gente decorar lá Quicksort, Mergesort, mas não explicou que mergesort foi criado pelo lendário Von Neumann pro problema de listas de dados que não cabiam na memória RAM da época, então começavam gravados numa fita. Daí o algoritmo vai percorrendo os elementos da fita, sem estourar a RAM, e vai gravando ordenado numa segunda fita. Se tiver essa imagem de fitas na cabeça, mergesort finalmente faz sentido. Sem isso, é um algoritmo que não faz sentido existir.</p>
<p>Em 2012 ainda não existia o equivalente mergesort pra grafos que não cabem em memória e precisam ficar divididos em partições de HDFS distribuídos em múltiplos servidores. Hoje em dia já existe um suporte maior. Pense um Apache Spark com GraphX. Mas 10 anos atrás, era um uso inédito ainda. O próprio paper descreve que eles estavam cientes dessas desvantagens. O paper menciona que o gargalo era na materialização dos vizinhos dos vértices, que requeria um enorme shuffling de dados, que é pegar dados de diversos servidores pra formar o grafo necessário pro processamento.</p>
<p>Nessa parte do paper ele referencia outro paper, o &ldquo;Discovering Similar Users on Twitter&rdquo;, onde é explicado como exatamente eles conseguiram passar as barreiras de usar Hadoop, adaptando o algoritmo de PageRank personalizado pra ambiente distribuído. Como já expliquei, esse algoritmo é largamente utilizado pra computar similaridade entre nós em um grafo, mas é iterativo e requer data shuffling frequente. Quando falo data shuffling é tipo reembaralhar dados. Pensa como isso é difícil quando seus dados estão distribuídos em diferentes servidores.</p>
<p>Pra resolver, os autores do paper propõe uma modificação ao algoritmo de PageRank personalizado chamado &ldquo;One Pass PPR&rdquo;, que reduz o número de iterações e minimiza esse reembaralhamento pra cada iteração. Nesse caso, os dados são particionados baseados nos IDs dos usuários e processados numa única passada sobre os dados, usando uma combinação de pontuações local e global. O algoritmo também usa técnicas de caching pra evitar recomputar resultados intermediários, reduzindo o número de iterações totais.</p>
<p>Além disso os autores também propõe técnicas de compressão pra reduzir o tamanho do grafo e possibilitar processamento mais rápido em sistemas distribuídos como Hadoop. É muito detalhe pra tentar explicar hoje, mas esse é o resumão desse outro paper que também recomendo darem uma olhada depois.</p>
<p>É nele que se resolve o problema de conseguir dizer que quem segue um Lebron James tem alta probabilidade de seguir um Kobe Bryant ou quem segue uma Lady Gaga costuma seguir uma Taylor Swift. E é onde nasce o framework que é utilizado até hoje e que vou retornar pra explicar, sobre a arquitetura de geração de candidatos e modelo de aprendizado baseado em regressão logística e similaridade de cosseno pra achar usuários similares no Twitter.</p>
<p>Voltando ao Cassovary, é só uma parte da solução, é quem dá acesso aos dados dos grafos em Hadoop. A solução geral é chamada de RealGraph, que é uma representação composta do grafo de seguidores e grafos de interação induzidos através de logs de comportamento. Essa representação é construída de múltiplos pipelines de funcionalidades que são extraídas, limpas, transformadas e juntadas a partir dos logs crús. O RealGraph é armazenado em HDFS. E esse nome &ldquo;RealGraph&rdquo; vamos encontrar no código em Scala do repositório que foi liberado, como podemos ver no VS Code. Toda vez que ver &ldquo;RealGraph&rdquo;, provavelmente é isso.</p>
<p>Mas esse segundo motor de recomendação ainda tinha um grande defeito, só servia pra recomendar usuários similares pra você seguir e só atualizava poucas vezes ao dia, não era nem perto de ser tempo real. Funcionava, mas ainda não é a aba &ldquo;pra você&rdquo; que tem no Twitter hoje. Pulamos do WTF de 2010, pro Cassovary, Hadoop e RealGraph de 2012, e agora saltamos pra 2014 e um novo paper que descreve algumas novas estratégias pra particionar e organizar esses grafos. Como podem ver, não era um problema trivial de resolver.</p>
<p>No caso de Twitter, e até em sistemas de mensageria em geral, existe uma particularidade nos dados: você nunca precisa do grafo inteiro dos usuários. Por exemplo, pouca gente tá interessado em ver tweets de mais de 1 ou 2 dias atrás das pessoas que segue. Então mesmo que seu influencer favorito tweet todos os dias, o que seria um volume grande de tweets pro grafo, podemos limitar uma janela de 1 ou 2 dias pra todo mundo e lidar com um sub-grafo muito menor.</p>
<p>Pensando assim não precisamos lidar com operações como &ldquo;deletar&rdquo; um tweet do grafo, por exemplo. Basta pensar em expiração. É mais ou menos o mesmo pensamento por trás de caches como Memcached ou Redis da vida, que usamos hoje. Não pense em como apagar dados velhos, estabeleça uma janela de tempo e deixa expirar sozinho, que é boa prática de qualquer tipo de caching.</p>
<p>Não só isso, mas não precisamos guardar o timestamp exato de cada tweet no grafo. Isso vem da observação que a maioria das interações, como likes, retweets, replies, costumam acontecer muito próximo do momento que o tweet foi criado. Pra maioria de nós é interessante saber que o tweet foi postado 1 hora atrás, 5 horas atrás, 1 dia atrás, mas não interessa exatamente quando. É menos um dado pra ter em memória e mais uma economia.</p>
<p>Pensando em tudo que eles aprenderam até agora, iniciariam um terceiro motor de recomendação em cima de outro projeto que eles também tornariam open source, chamado de GraphJet. Ele mantém um grafo bipartido que registra integrações de usuários a tweets durante as mais recentes &ldquo;n&rdquo; horas, com uma API super simples só com as operações que eles sabiam que realmente iam precisar, sem fazer overengineering de criar um banco de dados de grafos genérico demais. Finalmente tinham experiência suficiente pra conseguir especializar a solução.</p>
<p>O resto do paper entra em detalhes sobre como foi a estratégia de mapeamento de IDs, estratégias de gerenciamento de memória pra balancear reembaralhamento e distribuição dos dados. Em resumo, eles conseguiram chegar num modelo onde só o processamento pra encontrar usuários e tweets recomendados ficou na casa abaixo dos 10 milissegundos, que é perto do ideal pra conseguir ter resultados em real-time, onde a maior parte dos gargalos era a comunicação com os diversos sistemas de filas, como o Apache Kafka que usam até hoje.</p>
<p>Esse paper é bem bacana e recomendo lerem com muita calma e anotando cada termo que não conhecem pra pesquisar depois. Ele descreve as três gerações de motores de recomendação dentro do Twitter até 2016. Não parei pra ver se tem papers mais novos que continuam essa história, mas graças a isso o código liberado que tem vários trechos que mencionam RealGraph ou GraphJet, agora devem fazer mais sentido. Entenderam? Sem esse tipo de pesquisa, só lendo o código liberado, ninguém nunca ia entender do que se trata. Precisa do contexto histórico.</p>
<p>E isso é só a parte do motor de recomendação, existem diversas outras funcionalidades no Twitter, por exemplo, o motor de pesquisa, que é quando você faz pesquisas por palavras-chave e ele retorna tweets com esse conteúdo, que nem uma pesquisa no Google. Na figura da solução do Cassovary, que puxa dados do HDFS, que vieram do FlockDB, pra cima tem fetchers que mandam pra um serviço chamado Blender.</p>
<p>Blender foi um front-end escrito em Java em cima do Netty, que é a grosso modo o equivalente a um Node.js pra Java. Por baixo usa o lendário Apache Lucene como motor de procura. Lucene é feito em Java e a gente usa faz anos. Toda vez que um site tiver um campo de pesquisa, e os resultados forem bons, pode ter certeza que tem Lucene por trás. O produto mais famoso que é construído em cima do Lucene é o Elastic, também conhecido como Elasticsearch, ou o Solr da própria Apache. Full text search, correção ortográfica, auto complete, tudo isso já é problema resolvido por Lucene, Solr ou Elastic.</p>
<p>Só encontrei um post no blog de engenharia do Twitter e outro post de um ex-programador que trabalhou no Twitter por volta de 2012, mas além do Blender existia um outro sistema relacionado à parte de pesquisa em tempo real, que é diferente do sistema de recomendação, chamado Earlybird. E eu menciono isso porque &ldquo;Earlybird&rdquo; é outro termo que aparece bastante no código que foi liberado recentemente. Então agora você sabe.</p>
<p>Aproveitando que comecei a falar de projetos open source do Twitter, na parte do paper onde ele explica o desenvolvimento da solução GraphJet, falei que tem uma seção que fala da estratégia de IDs. Isso gerou outro projeto open source chamado Snowflake, que eu não conhecia também. Mas em dados distribuídos, IDs únicos é um problema. Por exemplo, numa tabela de banco de dados MySQL, rodando na sua máquina local, você coloca um inteiro auto incremental e esquece. Cada nova linha vai ganhando um ID como 1, 2, 3 &hellip; 10, 11, 12 &hellip; 1001, 1002 e assim por diante.</p>
<p>Ids incrementais tem diversos problemas, um deles é segurança, mas o outro é conseguir particionar esses IDs. E quando eu tenho um banco de dados distribuídos em 2 servidores? Vou só dizer que no primeiro servidor vai de 1 a 1 milhão, no segundo servidor vai de 1 milhão a 2 milhões? Isso não escala. Se você for mais esperto vai lembrar, ah, podemos usar coisas como UUID ou GUID da Microsoft, IDs de 64 bits ou 128 bits aleatórios. Ou até espaços menores de 48 bits, como MAC address de placas de rede.</p>
<p>Quanto maior o ID, mais espaço vai ocupar em disco e quando carregar em memória. E se vamos desperdiçar esse espaço, o ideal é que não seja aleatório e sirva algum propósito. Daí temos IDs semi-aleatórios. No caso da estratégia do Snowflake o ID é um número aleatório prefixado com um timestamp, um identificador do servidor onde está, e uma sequência numérica aleatória. Dessa forma consigo agrupar tweets que foram criados num determinado período de tempo, tenho a localização no cluster de servidores, mas também sei que não vai ter colisão desse ID com outro ID em outro servidor. Quer dizer, até pode ter colisão mas a probabilidade é hiper baixa, perto de zero, na prática.</p>
<p>Menciono isso porque se procurar no VS Code, olha só, Snowflake aparece em diversos lugares do código. E achamos trechos que usam a função <code>timeFromIdOpt</code> onde passamos um ID de Snowflake e ele retorna um timestamp, daí podemos fazer operações, como ver se foi criado antes ou depois de determinada data. Se estudar bancos de dados distribuídos, vai ver como existem diferentes estratégias de IDs únicos pra diferentes necessidades.</p>
<p>Um banco de dados como Cassandra usa outra estratégia, chamada TimeUUID, que é muito similar, já que também tem um componente de timestamp embutido no ID, o que facilita fazer queries de períodos de tempo e análise de time series, séries de tempo e processamento de eventos. Um DynamoDB da Amazon tem um identificador único chamado de &ldquo;chave de partição&rdquo; que é usado pra particionar dados em múltiplos servidores no cluster. A chave de partição é combinado com uma &ldquo;chave de ordenação&rdquo;, que é usado pra ordenar os dados dentro de uma partição.</p>
<p>Sem fugir demais do assunto, mas já que falei disso, preciso pelo menos explicar outro conceito. Em bancos de dados distribuídos, um problema seria se um servidor ficar mais cheio que outros servidores. Idealmente queremos balancear o volume de dados entre cada servidor. Uma das formas de fazer isso é o que chamamos de &ldquo;Consistent Hashing&rdquo;. Esse algoritmo mapeia chaves de partição, como o Snowflake do Twitter ou TimeUUID do Cassandra em nós virtuais num anel, um ring. Cada nó virtual representa um servidor físico no cluster.</p>
<p>À medida que novos nós são adicionados ou removidos do cluster, os ranges de partição são redistribuídos por todos os nós virtuais, garantindo distribuição uniforme das chaves de partição. Novamente, é um problema bem conhecido e bem resolvido nos mais diversos bancos de dados distribuídos. Eu lembro quando líamos papers da Amazon sobre isso lá por 2008 e 2009. Mas pra um estudante de ciência da computação hoje, continua sendo um problema fascinante de entender, já que o conceito é bem simples. Um ID é muito mais do que só uma etiqueta qualquer. Ela serve pra facilitar pesquisas e pra facilitar balanceamento de sistemas distribuídos.</p>
<p>E pra terminar essa parte de open source, tem outros dois projetos que são mencionados que acho importante explicar. O primeiro se chama Thrift e o segundo é Protocol Buffers ou Protobufs. Acho que muitos de vocês já devem ter ouvido falar pelo menos de Protobufs. Bem a grosso modo, pense micro-serviços ou endpoints de APIs hoje. A maioria de vocês deve estar acostumado a ter endpoints que se comunicam ou simplesmente cospem dados em formato JSON. É o hello world de APIs em todo tutorial e curso. Só que JSON não é a única forma e pra muitos casos, é a pior opção.</p>
<p>JSON é um formato excelente pra endpoints de APIs que seres humanos como eu ou você vamos manipular. É indicado pra coisas como APIs de ecommerce, onde queremos puxar coisas como principais ofertas, pra mostrar no nosso blog. Mas pra comunicação entre sistemas, por exemplo, o backend do ecommerce conversar com meios de pagamento ou logística. Ou mesmo pra comunicação entre micro-serviços dentro da mesma empresa, é um dos protocolos mais porcaria que existem.</p>
<p>Como um exemplo bem, mas bem tosco, imagine este pequeno JSON de um produto de ecommerce. Quem já trabalhou com ecommerce sabe que é bem maior que isso, mas pra ilustrar aqui no video é suficiente:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>{
  &#34;product_id&#34;: &#34;PS5-001&#34;,
  &#34;name&#34;: &#34;PlayStation 5&#34;,
  &#34;description&#34;: &#34;The PlayStation 5 is the next generation of gaming consoles, with advanced graphics, haptic feedback, and lightning-fast load times.&#34;,
  &#34;price&#34;: 499.99,
  &#34;availability&#34;: true,
  &#34;images&#34;: [
    {
      &#34;url&#34;: &#34;https://example.com/images/ps5-001-front.jpg&#34;,
      &#34;type&#34;: &#34;front&#34;
    },
    {
      &#34;url&#34;: &#34;https://example.com/images/ps5-001-back.jpg&#34;,
      &#34;type&#34;: &#34;back&#34;
    }
  ]
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>JSON é um protocolo texto, onde o schema vai embutido junto com cada registro. Como falei, isso facilita pra pessoas comuns conseguirem ler sem muita dificuldade. Agora imagine um schema de Protobuf assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>message Product {
  string product_id = 1;
  string name = 2;
  string description = 3;
  float price = 4;
  bool availability = 5;
  repeated Image images = 6;
}

message Image {
  string url = 1;
  string type = 2;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Note, ele define exatamente que tipo de dados, portanto qual o tamanho exato, de cada campo de dados. Dessa forma não preciso de coisas como aspas, vírgulas, nada disso, posso concatenar um valor atrás do outro, porque o tamanho vai ser fixo. No final, o mesmo JSON, convertido em Protobuf vai gerar um linguição binário que, em hexadecimal, seria assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>0a 07 50 53 35 2d 30 30 31 12 10 50 6c 61 79 53 74 61 74 69 6f 6e 20 35 1a 42 54 68 65 20 50 6c 61 79 53 74 61 74 69 6f 6e 20 35 20 69 73 20 74 68 65 20 6e 65 78 74 20 67 65 6e 65 72 61 74 69 6f 6e 20 6f 66 20 67 61 6d 69 6e 67 20 63 6f 6e 73 6f 6c 65 73 2c 20 77 69 74 68 20 61 64 76 61 6e 63 65 64 20 67 72 61 70 68 69 63 73 2c 20 68 61 70 74 69 63 20 66 65 65 64 62 61 63 6b 2c 20 61 6e 64 20 6c 69 67 68 74 6e 69 6e 67 2d 66 61 73 74 20 6c 6f 61 64 20 74 69 6d 65 73 2e 20 32 25 15 01 20 f3 26 9d 3a 40 28 01 32 2b 0a 1f 68 74 74 70 73 3a 2f 2f 65 78 61 6d 70 6c 65 2e 63 6f 6d 2f 69 6d 61 67 65 73 2f 70 73 35 2d 30 30 31 2d 66 72 6f 6e 74 2e 6a 70 67 22 04 66 72 6f 6e 74</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>É isso que a nova API com Protobuf vai cuspir. Como disse antes, ao contrário do JSON, esse formato é 100% hostil pra um ser humano conseguir ler, porém, é 100% perfeito pra um computador ler. A fase de converter o JSON numa estrutura de dados é muito mais simples. Um protobuf é muito próximo de uma struct de C, só que mais genérico pra funcionar em qualquer linguagem. O JSON do exemplo é um string que pesa quase 200 bytes. Já esse linguição, que contém exatamente os mesmos dados, pesa menos de 80 bytes. Mesmo nesse exemplo super tosco, estamos falando de uma economia de 2.5 vezes na transferência de rede, sem contar a economia de processamento.</p>
<p>Lembra que falei que o Twitter lida com milhões de tweets todo dia? Imagine se internamente, em todo sistema, de pesquisa, banco de dados distribuído, sistema de recomendação, filas de processamento, ficasse trafegando tudo em JSON. Terabytes e terabytes de banda sendo desperdiçados. Por isso o Twitter adotou o equivalente a Protobufs do Google, o Thrift do Facebook. Thrift e Protobuf é quase a mesma coisa.</p>
<p>A única pergunta que você poderia ter é: por que o Twitter não usou o Protobuf do Google então? Em vez de reinventar a roda? De fato, Protobufs foram criados antes, sendo usados dentro do Google desde pelo menos 2001, mas só foi lançado publicamente em 2008. Nesse meio tempo surgiu o Facebook, que teve problemas similares pra resolver e inventaram o Thrift, que lançaram em Abril de 2007. Por isso o Twitter só tinha a opção do Thrift em 2007. Mas pra novos projetos, especialmente que interagem com componentes como PyTorch ou Tensorflow, que também veio do Google, eles passaram a usar Protobufs e você vai achar os dois no código.</p>
<p>Thrift e Protobufs são protocolos de serialização de dados. Esse tipo de protocolo é ideal pra comunicação entre micro-serviços. Só que diferente de JSON ou XML, a forma de fazer essa comunicação não é via HTTP, como num web service, e sim via RPC ou Remote Procedure Call, que formaliza como um serviço chama uma função, ou procedure, de outro serviço. No caso do Google eles tem o gRPC que é um framework de RPC que tem como objetivo prover comunicação eficiente e escalável entre serviços na rede.</p>
<p>Twitter não usou gRPC. Finagle é o equivalente de gRPC feito pelo Twitter, que usa Thrift pra serialização de dados em vez de Protobufs. Tem os mesmos objetivos de lidar com comunicação de alto throughput, baixa latência, na rede. Finagle é implementado em cima de Netty também, usando muita funcionalidade de programação assíncrona com Futures de Scala, que é o equivalente Promises de Javascript de hoje. E novamente, o Twitter Finagle foi lançado em 2011, o gRPC do Google só sairia por volta de 2015. O Twitter estava tendo que inventar tecnologias que não existiam ainda, por isso é um case fascinante.</p>
<p>Mas claro, Google sendo um nome muito maior e mais reconhecido que Twitter, matou os projetos Thrift e Finagle. Num projeto moderno, você deveria escolher Protobufs e gRPC ou alternativas mais modernas. Hoje, o Thrift ainda existe como Apache Thrift mas da Apache temos outras alternativas como Apache Avro, que foi criado mais pro cenário de streaming de dados serializados. Ou seja, quando você abre um streaming de um serviço pra outro serviço e trafega registros serializados com Avro entre eles.</p>
<p>Apache é um nome muito reconhecido pra quem é de Java e também no espaço de data sciences e machine learning. Quem lida com Kafka e é de Java, deve ter usado Avro como opção de serialização pra jobs em vez de Protobufs ou JSON. JSON é mais pra linguagens menores, como Javascript ou mesmo Ruby e PHP. Protobufs são mais usados em soluções que envolvam tecnologias do Google, como em Android. De qualquer forma, vale a pena estudar todas. Não ache que o mundo acaba em JSON ou XML. Tem coisa muito melhor e muito mais eficiente que é usado por sistemas realmente de grande volume. Pro ecommerce da esquina, de fato, tanto faz.</p>
<p>Falando em Java, acho que vale a pena eu relembrar a parte inicial dessa história. O que aconteceu justamente entre 2007 a 2010. Essa era a época que eu era mais participativo na comunidade Ruby on Rails e, claro, Twitter era um dos exemplos de maior sucesso de Rails. Mas aí veio a notícia bombástica: o Twitter declarou que estava migrando de Ruby pra Scala. E daí veio o meme que não morre de &ldquo;Rails não escala&rdquo;.</p>
<p>A forma como o Alex Payne, um dos principais desenvolvedores do Twitter descreveu, ou melhor, não descreveu os problemas que estavam enfrentando, tudo que eu expliquei até aqui, causou má interpretação e muita gente falando muita merda. Eu conversei com outro dos principais desenvolvedores daquela época, Blaine Cook, em junho de 2008. E mesma naquela época, eu também não tinha uma visão completa dos acontecimentos. Depois leiam a entrevista original que está no meu blog.</p>
<p>Por exemplo, da forma como Alex Payne falou, deu impressão que ele estava dizendo que Ruby on Rails não suportava conectar com múltiplos bancos de dados, o que não era verdade. Sim, não havia uma chavinha fácil, mas era basicamente uma linha extra que precisava ser colocada na classe pai de todos os ActiveRecords e dava pra selecionar onde fazer a conexão. Eu e vários outros escrevemos tutoriais de como fazer isso.</p>
<p>Mas a verdade é que o problema não era esse. Ninguém de fora do Twitter tinha muita noção do volume de dados que eles estavam tendo que lidar. E isso era 2007. Lembrem-se, a Amazon AWS tinha lançado não fazia um ano. Produtos que hoje são arroz com feijão, como SQS, DynamoDB, Route53 nem tinham sido lançados ainda. Firebase ainda não existia. Porra, coisas como Redis ainda não tinha sido inventado. GitHub ainda não tinha sido inventado.</p>
<p>A verdade é que particionar dados, organizar um MySQL em shards, e conseguir fazer uma arquitetura coesa em cima disso, não era trivial, e o Twitter estava numa fase de crescimento rápido. Não é que Ruby on Rails não escalava pra escala do Twitter. Nada em Python escalaria. Nada em PHP escalaria. Node.js ainda não tinha sido inventado também, mas também não escalaria. Entenda, havia poucas opções.</p>
<p>Possivelmente a opção mais completa teria sido começar a implementar um banco de dados de grafos em cima de Erlang, algo inspirado no banco de dados Mnesia que vem no OTP de Erlang. Porém, já viram Erlang? Hoje ele ainda é pouco usado, e em 2007 menos ainda. É verdade que grandes sistemas de telecomunicações já usavam fazia anos, mas era um salto bem grande pra uma tech startup tomar.</p>
<p>Quais as alternativas? Eles precisavam de uma linguagem pra escrever componentes como sistemas de filas, coisas no nível de Kafka, lembrando que Kafka só seria lançado em 2011. Coisas no nível de Cassandra, que também só seria liberado publicamente pelo Facebook em 2008. Neo4J talvez fosse o componente mais próximo de se encaixar nos problemas do Twitter, mas era super novo, tinha sido lançado em 2007.</p>
<p>Entendem? Imagine você sendo um desenvolvedor do Twitter, vendo milhares, centenas de milhares de tweets sendo postados todos os dias, e tendo que lidar com grafos de usuários e tweets, sendo que algoritmos de grafos nunca tinham sido testados com volumes desse tipo. Numa época onde toda tecnologia de Cloud e NoSQL e Devops ainda não tinha sido inventado, onde linguagens mais comuns como Ruby, Python, PHP, Javascript, não estavam ajudando e programar em C++ ou Java, parecia ser um passo pra trás.</p>
<p>Elixir não tinha sido inventado ainda. Go não tinha sido lançado ainda. Rust não tinha sido inventado ainda. LLVM ainda era uma grande novidade, já que a Apple tava só começando a mover suas ferramentas e linguagens de GCC pra Clang, como conto no episódio da Apple e GPL. Mas havia uma única linguagem que talvez pudesse fazer sentido, a linguagem Scala, que tinha sido lançado já em 2001, pelo visionário Martin Odersky.</p>
<p>Odersky é um desses gênios incompreendidos, tanto que tirando povo da área de Java, duvido que qualquer um aqui já tenha ouvido falar dele. Mas deveriam. O cara é Ph.D de ciência da computação pela ETH de Zurique e trabalhou sob a supervisão de ninguém menos que Niklaus Wirth, o inventor de várias linguagens como Modula-2 e Pascal, e o autor do livro de estruturas de dados e algoritmos que eu usei na faculdade.</p>
<p>Odersky participou e fez contribuições no mundo Java desde a versão 1.1, tendo trabalhado no compilador, na implementação de Generics, e algumas linguagens experimentais como a Pizza e Funnel, que influenciaram o design do Java.</p>
<p>Muito antes de programação funcional ficar hypezinho de hipster sem graça, como é hoje, no começo do século, ele já vislumbrava uma linguagens que destilasse o melhor da orientação a objetos, como usar traits, que são como protocolos de Objective-C, em vez de herança excessiva, mesclado com conceitos de programação funcional. Scala já tinha higher order functions, já usava tipo Opcional, evitando os famosos NullPointerException, pattern matching e tudo que se vê em linguagens mais modernas de hoje.</p>
<p>Então era de fato a linguagem mais moderna que se podia escolher em 2007, mas rodando em cima da JVM, que era a virtual machine mais madura que existia. Dava pra criar aplicações que só seriam possíveis em C++ com uma fração da dor de cabeça. Pra isso custaria um pouco mais caro em uso de recursos do que se fosse puramente nativo, mas a JVM naquele ponto oferecia um ecossistema que ninguém mais tinha.</p>
<p>E o objetivo do uso de Scala não era pra fazer front-end-zinho, era pra fazer os diversos componentes que eu expliquei que ainda não existiam. Começou com o projeto open source Starling, que é mais ou menos um mini Kafka, alguns anos antes do Kafka. Inventaram o RealGraph e GraphJet antes de um banco de dados de grafos maduro existir. Utilizaram coisas como Hadoop, que também faz parte do ecossistema Java. Nada disso seria possível sem ser, ou em Java ou em C++.</p>
<p>Hoje em dia eles teriam muito mais opções, como o próprio Elixir, Go ou Rust, e se quisessem arriscar em linguagens mais exóticas teriam D, Nim, Zig, talvez Swift, Kotlin. Mas de novo, era 2006 pra 2007 e o relógio tava correndo. Em retrospecto, foi uma decisão muito acertada. E é por isso que o código que foi liberado é 2 terços Scala e Java. Muito das pesquisas que o Twitter fez, junto com trabalhos do Facebook, Google, Apple, Amazon, gerou o atual ecossistema de linguagens, ferramentas e plataformas pra lidar com Big Data, Data Science, Machine Learning, culminando com Deep Learning e a atual geração de inteligência artificial que todo mundo tá babando. Mas foi assim que as coisas começaram, no início do século.</p>
<p>Agora que temos uma noção melhor do contexto histórico, dos projetos e papers de pesquisa gerados, podemos começar a entender melhor o código liberado. Esse código é um conjunto de diversos sub-projetos. Se abrirmos o primeiro README na raíz do repositório, eles descreveram o que são cada um desses projetos. Por exemplo, SimClusters-ann é o serviço que retorna candidatos de recomendações de tweets dado embeddings de um cluster de similaridade.</p>
<p>Esse serviço usa o algoritmo de similaridade aproximado de cossenos. Já vou explicar isso. Segundo o README, um job constrói o mapeamento entre SimClusters e Tweets. O job grava os top 400 tweets de um SimCluster e os top 100 SimClusters pra um tweet. Pontuação de favoritos e pontuação de seguidores são dois tipos de pontuação que um tweet pode ter. Os top 100 SimClusters baseados nessas pontuações é que chamam de Tweet SimCluster Embedding.</p>
<p>A similaridade de cosseno entre dois Embeddings apresenta o nível de relevância de dois tweets num espaço de SimClusters. A pontuação varia de 0 a 1. A alta pontuação de similaridade de cosseno, maior que 0.7 significa que os usuários que gostam de dois desses tweets compartilham os mesmos SimClusters.</p>
<p>Similaridade de cosseno é um conceito de Álgebra Linear pra medir quão similar são dois vetores em termos de direção e magnitude. Eu gosto desse conceito porque expliquei isso desde pelo menos 2015 até minha última palestra pública em 2019. Quem assistiu minha palestra &ldquo;Restrição == Inovação&rdquo; vai se lembrar disso, e vou aproveitar pra usar os slides que eu usava nessa palestra.</p>
<p>Comece visualizando a página inicial do Google, o campo onde se digita palavras-chaves. Podemos digitar &ldquo;apple&rdquo; e recebemos páginas indexadas da web que tem essa palavra. Um amador que acabou de aprender SQL poderia pensar que é alguma coisa parecido com &ldquo;select urls from pages where text like &lsquo;%apple%&rsquo;&rdquo;.</p>
<p>Essa é a solução que faríamos no começo dos anos 90, antes de Larry Page e Sergey Brin inventarem PageRank, antes de Jon Kleinberg inventar HITS. Essa linha de SQL de fato vai achar todas as páginas onde aparece a palavra &ldquo;apple&rdquo;, mas não vai saber como ordenar: qual delas é mais relevante e deveria estar no topo da lista? Pra saber relevância, precisamos de Álgebra Linear.</p>
<p>Tudo depende de como você enxerga o que é uma página web. Todo mundo enxerga um documento, uma coleção de palavras, que é o texto desse documento. Digamos que nosso banco de dados só tem 3 documentos indexados, como neste slide. Pra simplificar bastante, vamos assumir que nosso vocabulário é bem limitado e os documentos só tem as palavras apple e banana. O documento 1 tem 4 apples e 1 banana, documento 2 tem 3 bananas e 1 apple, e documento 3 tem 5 bananas e nenhum apple.</p>
<p>Podemos representar esses documentos como vetores, como  neste slide. Próximo passo, criamos um índice, que é uma lista que diz em quais documentos aparece a palavra apple e quais aparece banana. Pense em algo como o índice do seu banco de dados MySQL ou Postgres ou SQL Server da vida. Agora vamos digitar a palavra &ldquo;apple&rdquo; no campo de pesquisa desse nosso Google de mentira.</p>
<p>O truque começa em representar essa pesquisa, essa query, também com um vetor. No caso apple sendo 1 e banana sendo 0. É como se fosse um documento também. Pelo índice, podemos descartar o documento 3, já que apple não aparece nenhuma vez. Mas ainda temos o documento 1 e o documento 2. Qual deles é o mais relevante pra aparecer no topo da lista? De bater o olho sabemos que é o documento 1, porque aparecem a palavra apple 4 vezes, e no documento 2 só aparece 1 vez. Mas como um algoritmo conseguiria computar isso?</p>
<p>Pra isso desenhamos esses vetores num espaço vetorial, um Vector Space. No caso só temos duas dimensões, porque só temos duas palavras no nosso vocabulário. Apple é o eixo X e banana é o eixo Y. Então o vetor que representa o documento 1 seria assim: 4 unidades pra direita no eixo X, e 1 unidade pra cima no eixo Y. Já o documento dois seria só 1 unidade pra direita no eixo X mas 3 unidades pra cima no eixo Y. Finalmente o vetor da query é só uma unidade pra direita no eixo X, só 1 palavra apple, entenderam?</p>
<p>O lance com vetores é que agora temos ângulos entre o vetor de pesquisa e os vetores de documentos. E pra calcular a similaridade de cosseno usamos dot product, que é produto escalar. Funciona assim: vamos multiplicar apple com apple, banana com banana e somar os dois. Então vetor de pesquisa é 1 e 0, 1 apple e 0 bananas e vetor do documento 1 é 4 apples e 1 banana então a conta seria 1 vezes 4 mais 0 vezes 1, que dá 4, esse é o produto escalar.</p>
<p>Daí fazemos a mesma coisa entre vetor de pesquisa, de novo, e o vetor do documento 2, então o produto escalar vai ser, 1 apple vezes 1 apple mais 0 banana vezes 4 bananas, dando 1. Como o produto escalar 4 do documento 1 é maior que o 1 do documento 2, sabemos que o documento 1 tem mais similaridade com a pesquisa e, portanto, é o mais relevante pra aparecer no topo da lista.</p>
<p>Esse exemplo é super trivial porque só usamos duas palavras, então temos só duas dimensões. Mas vamos complicar. E se tivéssemos um novo documento indexado que tenha a palavra coco? O vetor dele poderia ser 1 apple, 0 banana e 2 cocos. Todos os outros vetores precisam ser atualizados pra conter coco, mas no caso tem 0 cocos então não muda nada. Pra representar o espaço vetorial, precisamos de uma nova dimensão Z. O vetor desse novo documento seria desenhado com 1 unidade pra direita no eixo X e 2 unidades pra baixo no eixo Z.</p>
<p>E podemos calcular o produto escalar entre a pesquisa e esse novo documento também. Em geometria, a gente consegue desenhar no papel até 3 dimensões, mas claro, o vocabulário de uma língua como português é muito mais que só 3 palavras. Num dicionário como o Aurélio, vai ter mais de 500 mil palavras. Portanto podemos ter potencialmente 500 mil dimensões nesse espaço vetorial. Na realidade vai ser a quantidade de palavras únicas que tem num documento, ou página web de verdade. O script deste episódio, por exemplo, tem mais de 3 mil palavras únicas. É um vetor de 3 mil dimensões.</p>
<p>Felizmente, produto escalar é uma operação muito rápida de se calcular em qualquer computador moderno, só somas e multiplicações simples. E é assim que funciona a base de todo tipo de algoritmo de relevância e recomendação. É isso que tem por baixo da biblioteca Apache Lucene que falei antes. E se olhar a documentação de um Elastic vai ver que menciona VSM, ou Vector Space Model, que é esse gráfico que vim mostrando, muito maior do que os 3 do exemplo. E é isso que esse projeto SimClusters vai fazer, só que com vetores de usuários e tweets.</p>
<p>No caso do Twitter, esse primeiro projeto se chama SimCluster-ANN. SimCluster é similarity cluster, e esse &ldquo;ANN&rdquo; provavelmente significa &ldquo;Approximate Nearest Neighbor&rdquo; ou vizinho mais próximo aproximado. Em machine learning e data mining, pesquisa de vizinho mais próximo é o problema de encontrar o ponto de dado mais próximo em um espaço de muitas dimensões, dado um certo ponto de pesquisa, onde soluções exatas são inviáveis pelo tamanho do conjunto de dados.</p>
<p>Algoritmos de vizinho próximo aproximado, como Locality-sensitive hashing ou LSH e métodos baseados em árvore como KD-trees, oferecem uma forma eficiente de resolver esse problema fazendo um trade off de algum grau de precisão por uma melhoria significativa em performance. Então é mais uma otimização do Twitter pra ganhar performance, mais ou menos como o random walk no algoritmo de SALSA que expliquei antes.</p>
<p>Nesse sub-projeto, o conceito importante são embedding spaces, que tem como objetivo responder a pergunta &ldquo;quais tweets e usuários são similares aos meus interesses?&rdquo;. Embedding funciona gerando representações numéricas dos interesses dos usuários e conteúdo de tweets. Daí podemos calcular a similaridade entre quaisquer dois usuários, tweets ou pares de usuários e tweets num espaço de embedding.</p>
<p>Justamente um dos espaços de embeddings mais úteis do Twitter são os SimClusters. SimClusters descobrem comunidades ancoradas por um grupo de usuários influentes usando um algoritmo customizado de factorização de matrizes. Existem umas 145 mil comunidades, que são atualizadas a cada três semanas. Usuários e tweets são representados em espaço de comunidades e podem pertencer a múltiplas comunidades. Elas podem ter algumas milhares até milhões de usuários. Segundo o blog de engenharia, essa imagem representa algumas das maiores comunidades.</p>
<p>Sem entrar em muito mais detalhes, vamos bater o olho em alguns outros projetos descritos naquele arquivo README. Veja um aqui chamado &ldquo;real-graph&rdquo;. Eu falei de RealGraph antes. Veja este &ldquo;Tweepcred&rdquo;, que é o algoritmo de PageRank pra calcular reputação dos usuários. Temos esse &ldquo;recos-injector&rdquo; que é descrito como um processador de eventos em streaming pra construir os streams de entrada pra serviços baseados em GraphJet. Já sabemos o que é GraphJet, mas &ldquo;recos&rdquo; eu acho que significa &ldquo;recomendações&rdquo;, então é um injetor de recomendações pra GraphJet.</p>
<p>Pulando lá pra baixo temos esse &ldquo;light-ranker&rdquo; que é um ranqueador mais leve usado pelo índice de pesquisa, o tal Earlybird que mencionei antes, que é um projeto legado e parece que ainda é usado; o componente construído em cima de Apache Lucene. O heavy-ranker está no outro repositório &ldquo;the-algorithm-ml&rdquo;, que o blog post do Steven, que falei lá no começo, menciona e segundo o README é o responsável pela aba &ldquo;Pra você&rdquo; e tem aquela lista de pesos.</p>
<p>Nesse ponto da cadeia, depois da fase de candidate sourcing, temos uns 1500 candidatos que podem ser relevantes. Pontuação serve pra prever a relevância de cada tweet candidato e é o sinal primário pra ranquear tweets na timeline. Nesse estágio, todos os candidatos são tratados igualmente. O ranqueamento sai de um rede neural de uns 48 milhões de parâmetros que é continuamente treinada com interações de tweets pra otimizar o engajamento positivo (como likes, replies e retweets), segundo o blog de engenharia do Twitter.</p>
<p>Sendo bem honesto, eu não tenho certeza de como exatamente isso funciona ainda. Olha o diagrama que tem no próprio arquivo README que lista os projetos. Os dados vem dessas caixinhas intituladas &ldquo;Grafo Social&rdquo;, &ldquo;engajamento de tweets&rdquo; e &ldquo;dados dos usuários&rdquo;. Daí temos features, como o GraphJet, SimClusters, TwHIN que é outro projeto no repositório the-algorithm-ml, e serve pra pré-treinar os tais embeddings.</p>
<p>Daí temos o RealGraph, TweepCred e Trust &amp; Safety, que parece ser um projeto de tensorflow pra treinar modelos pra filtrar coisas como conteúdo pornográfico, adulto, ou o que ele considera como &ldquo;tóxico&rdquo;. Não sabemos exatamente porque não foi liberado o modelo e nem os dados de treino. É um projeto bem pequeno e quem entende de Tensorflow poderia dizer um pouco mais, mas não tem muito pra se ver.</p>
<p>Continuando, temos essa coluna que ele chama de Home Mixer. Daí temos 3 fases: uma que ele chama de candidate sourcing, que é literalmente &ldquo;escolher os candidatos&rdquo;, candidatos de recomendação. Esses candidatos passam pelo tal heavy ranker, que parece ser o modelo de machine learning via PyTorch. O resultado disso passa pra etapa final de heurísticas e filtros.</p>
<p>Então, pra se chegar nos tweets recomendados na aba &ldquo;Pra você&rdquo;, na etapa de sourcing já acontece diversos filtros pra selecionar os candidatos de tweets que você pode gostar de ver. Esses &ldquo;filtros&rdquo; passam pelos projetos de SimCluster, RealGraph, T&amp;S, Tweepcred, os GraphJets e são mesclados pelo projeto cr_mixer. Isso vale comentar. Os tweets recomendados, mais ou menos, são metade de tweets gerados pelas pessoas que você segue, o que ele chama de &ldquo;In-Network&rdquo; e a outra metade é de tweets de pessoas que você não segue, que ele chama de &ldquo;Out-of-Network&rdquo; e quem puxa isso é esse projeto cr-mixer.</p>
<p>Dado esse conjunto inicial de tweets, In e Out of network, aí vem a parte de recomendação propriamente dito usando o light ranker, o projeto Earlybird, e daí o heavy ranker. Mas antes de mandar pra você, ainda passa por um pipeline de filtros. Pelo projeto home mixer propriamente dito, esse visibility-filters que é onde está implementado a política de censura do Twitter.</p>
<p>Por último, tem esse projeto Navi, que parece mais recente, escrito em Rust, mas é super pequeno e não faz muita coisa. Parece mais um adaptador pra falar com os componentes de Tensorflow e PyTorch e um tal de Onnx, que eu não conhecia, mas parece que significa Open Neural Network Exchange, que é um formato open source pra representar modelos de machine learning. Foi originalmente desenvolvido pela Microsoft e Facebook em 2017 pra facilitar interoperabilidade entre diferentes frameworks de deep learning, por exemplo, entre PyTorch e Tensorflow. Mas realmente não sei como ele é usado neste projeto ainda.</p>
<p>Se tiverem interesse em saber mais, cada um desses sub-projetos tem um README também, então recomendo que comecem por eles. Mas como disse no começo, se tentar rodar <code>bazel build</code> em qualquer diretório, ele vai reclamar da falta de um arquivo de Workspace. Então, até o Twitter liberar isso, ou alguém se dar o trabalho de gerar na mão, não temos como compilar e nem de tentar rodar nada.</p>
<p>Por isso que é meio chato, porque mesmo lendo essas coisas todas na documentação e no próprio código, não temos como testar. Basta um if que a passamos batido e talvez algum desses componentes nem rodem. É muito difícil saber o comportamento exato dum sistema grande desses sem ter uma massa de dados de entrada pra ver o que realmente é cuspido na saída.</p>
<p>De qualquer forma, do ponto de vista de organização, além da falta de arquivos pra fazer build, também senti muita falta de arquivos de testes automatizados. Não tem como saber se eles tiraram antes de abrir em público, se ainda vai ser liberado, ou se realmente não existem. Espero que existam, porque dar manutenção num sistema grande desses, sem testes, seria uma grande burrice.</p>
<p>Se eles tivessem liberado os arquivos de testes, nós teríamos uma idéia muito melhor de como esse sistema de comporta, mesmo sem poder compilar e rodar. Testes servem como se fosse uma documentação viva de um sistema. Teria sido muito mais fácil confirmar tudo que analisamos.</p>
<p>Mas no geral, minha opinião é que o código Scala parece muito bem feito, dado o contexto que se trata de um legado de mais de 10 anos. Eu esperaria algo muito mais bagunçado, muito mais macarrônico. Comparado com o código do Twitch, que eu analisei em outro video quando foi vazado, eu preferiria muito mais trabalhar neste código do que no outro.</p>
<p>O básico tá bem feito: métodos curtos, baixa complexidade ciclomática, nomenclatura bem feita, nada de variáveis &ldquo;x&rdquo; ou &ldquo;y&rdquo; que não dá pra saber do que se trata. Pouca coisa hard-coded, tudo organizado em arquivos de configuração, e até que bem comentados onde precisa. Não é comum ver projetos legados com esse nível de qualidade. Não é nem de longe perfeito, mas até que me surpreendeu.</p>
<p>Obviamente, esse repositório é só um espelho incompleto do repositório de verdade, que permanece fechado dentro do Twitter, sob confidencialidade. O código que foi liberado também não pode ser usado por nenhum projeto fechado sem liberar as modificações feitas em cima desse código. É isso que a licença AGPL que eles colocaram significa: eles não querem que ninguém adicione em outro projeto e mantenha fechado.</p>
<p>Depois que soltaram esse código, um bando de imbecis ficou spamando a seção de Issues, mas teve um ou outro pull request que eles aprovaram já, nada significativo. Só a remoção daquele código de métrica que tinha o &ldquo;author_is_elon&rdquo; que imbecis adoraram ficar fazendo meme. Não teve nenhuma contribuição significativa ainda e nem espero que vá ter, até que eles liberem os arquivos de Workspace do Bazel.</p>
<p>Eu não tenho muito senso de humor pra memes idiotas e gente que acha bacana ficar pichando uma seção que deveria ser séria como a área de Issues. Quer fazer meme idiota, fica no Reddit ou no 4chan. Não tem o mínimo de etiqueta, não participa. Participação desse tipo é só idiota, não tem nenhum valor e não contribui com absolutamente nada. É só certificado de idiota mesmo.</p>
<p>Ao longo de mais de 10 anos, o Twitter fez sua parte e contribuiu tecnicamente. Os papers que falei, os projetos open source, os diversos blog posts de engenharia, inspiraram muitos outros projetos a tentar arquiteturas e tecnologias que, na época, sequer existiam ou se existiam, ninguém sabia se funcionava. Independente da sua opinião política, que é irrelevante, o Twitter certamente contribuiu ordens de grandeza mais do que qualquer um dos engraçadinhos que não sabem respeitar um trabalho de engenharia.</p>
<p>Ter acesso a esse código não me torna mais fã do Elon Musk, mas só serve pra confirmar muitas coisas que eu já sabia sobre os primeiros engenheiros do Twitter: eles foram muito bons, especialmente pra época. E pra vocês que são iniciantes, aproveitem a oportunidade pra fuçar tanto o código quanto os papers que eu citei. É assim que um produto em produção evolui. Só nesse sistema de recomendações, vimos 3 gerações diferentes de soluções, cada uma gerando mais experiência pra fazer o próximo ser melhor. Código de produção de verdade, de um produto que tem uso, nunca acaba.</p>
<p>Você que é empreendedor querendo entender mais de programação, entenda: se seu código não está evoluindo, é porque seu produto é irrelevante. O Twitter existe desde 2006, e o código nunca chegou numa versão que todo mundo tirou a mão e falou &ldquo;tá perfeito, não precisa mexer mais&rdquo;. O mesmo acontece com qualquer produto, ecommerce, elearning, fintech, todos continuam evoluindo, pivotando, ajustando e consertando pra ficar cada vez melhor. O Twitter de hoje é bem diferente do Twitter de 2007, mesmo que só pelo front-end não pareça que tenha mudado muita coisa. A cara não muda tanto quanto o backend.</p>
<p>Dá pra ficar literalmente dias, indo de arquivo em arquivo, tentando desvendar quem chama quem, quem depende de quem, em que momentos, quais serviços rodam pra quais tipos de dados. Mas é super trabalhoso fazer isso sem conseguir rodar nada e sem testes. Eu não acho que vale a pena, mas na internet sempre tem alguém com mais boa vontade de fazer trabalho chato como esse. Com apenas 2 dias olhando pra esse código, eu só consigo descrever até aqui.</p>
<p>E com tudo isso, pelo menos espero que pra vocês que sejam iniciantes, tenham tido uma perspectiva diferente do que imaginavam que seria um código de verdade de produção. Mesmo sem entender nada, deve dar pra começar a entender melhor a dimensão. Repetindo: esse repositório liberado só tem parte do código do sistema de recomendação. Não tem o front-end web, não tem o app mobile, não tem as ferramentas de administração, não tem o ferramental de monitoramento, enfim, só estamos vendo uma fração do que é feito o Twitter.</p>
<p>Antes de terminar, enquanto estava escrevendo este episódio, o canal Fireship lançou também um video curto falando disso. Não tem nada demais, mas eles lançaram um segundo especialmente sobre &ldquo;Vector databases&rdquo;. Só assistindo esse video dele, duvido quem a maioria tenha entendido. Eu expliquei o que é isso durante este video, e se você já não tinha conhecimento prévio de Álgebra Linear, provavelmente entendeu mas não compreendeu.</p>
<p>Enfim, o Fireship menciona bancos de dados modernos de vetores como Pinecone e Weaviate e disse que vai fazer um curso sobre isso. Não sei se foi piada, como eles sempre fazem, ou se vão mesmo, mas vou ficar de olho pra saber. Problema de um curso desses é o famoso &ldquo;olha como se faz um hello world, é super simples&rdquo;, mas sem entender tudo que falei hoje, você vai usar sem entender. Mas assistam lá depois. Vou deixar o link na descrição abaixo.</p>
<p>E finalmente, isso responde outra pergunta que todo mundo tem: precisa saber matemática pra ser programador? E a resposta é não. Pra fazer coisa simples, sabendo matemática básica de regra de três e porcentagem, dá pra ir se virando. No dia a dia de software pequeno, se souber pelo menos usar uma calculadora, já funciona. Agora, sem saber Álgebra Linear, Cálculo, Estatística e Probabilidade, você nunca vai trabalhar em projetos como esse do Twitter, é uma impossibilidade. Matemática é opcional, inglês é opcional, tudo depende de quão pouco você quer ter de opções. Se ficaram com dúvidas, mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e não deixem de compartilhar o video com seus amigos. A gente se vê, até mais.</p>
]]></content:encoded><category>algoritmos-e-estruturas-de-dados</category><category>machine-learning</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #127 - Como Funciona o Boot de um Linux? | O que tem num LiveCD?</title><link>https://akitaonrails.github.io/2022/09/12/akitando-127-como-funciona-o-boot-de-um-linux-o-que-tem-num-livecd/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/09/12/akitando-127-como-funciona-o-boot-de-um-linux-o-que-tem-num-livecd/</guid><pubDate>Mon, 12 Sep 2022 18:05:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/5F6BbhgvFOE"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Instalar distros Linux hoje em dia é muito fácil. Só bootar do pendrive, clicar em &amp;ldquo;próximo&amp;rdquo;, &amp;ldquo;próximo&amp;rdquo; e pronto, tá tudo instalado e funcionando. Qualquer um consegue instalar na maioria das configurações modernas de hardware.&lt;/p&gt;
&lt;p&gt;Porém, isso não te ensina nada e ao final você continua não sabendo absolutamente nada sobre o que é um Linux e o que de fato ele faz. Como é o processo de boot? Quais softwares estão envolvidos nesse processo? E mais importante: o que posso fazer quando alguma coisa dá errado? Quais ferramentas tenho à minha disposição?&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/5F6BbhgvFOE"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Instalar distros Linux hoje em dia é muito fácil. Só bootar do pendrive, clicar em &ldquo;próximo&rdquo;, &ldquo;próximo&rdquo; e pronto, tá tudo instalado e funcionando. Qualquer um consegue instalar na maioria das configurações modernas de hardware.</p>
<p>Porém, isso não te ensina nada e ao final você continua não sabendo absolutamente nada sobre o que é um Linux e o que de fato ele faz. Como é o processo de boot? Quais softwares estão envolvidos nesse processo? E mais importante: o que posso fazer quando alguma coisa dá errado? Quais ferramentas tenho à minha disposição?</p>
<p>Hoje é o dia de finalmente você começar a entender como sua máquina realmente funciona.</p>
<p>== Conteúdo</p>
<ul>
<li>00:00 - Intro</li>
<li>01:11 - CAP 1 - Instalar Linux é Fácil | Ubuntu 22.04</li>
<li>06:11 - CAP 2 - O que é um Mount Point? | Tudo em Linux são Arquivos</li>
<li>11:45 - CAP 3 - Firmwares e UEFI | Quem começa o boot?</li>
<li>15:43 - CAP 4 - O que é uma “imagem” ou ISO? | O que tem num LiveCD?</li>
<li>26:03 - CAP 5 - O “meta-Linux” antes do Linux | Init RAM</li>
<li>31:26 - CAP 6 - Depois do Linux: Daemons | Systemd</li>
<li>33:31 - CAP 7 - O que são “Run Levels”? | O jeito SysV antigo</li>
<li>40:40 - CAP 8 - O fim do Init SysV? | O jeito novo do Systemd</li>
<li>45:04 - Bloopers</li>
</ul>
<p>== Links</p>
<ul>
<li>gnome-cedilla-fix (<a href="https://github.com/marcopaganini/gnome-cedilla-fix"target="_blank" rel="noopener">https://github.com/marcopaganini/gnome-cedilla-fix</a>)</li>
</ul>
<p>== SCRIPT</p>
<p>Olá pessoal, Fabio Akita</p>
<p>Por hora, finalizei finalmente a minissérie sobre redes e internet e agora é hora de falar um pouco mais sobre Linux  pra complementar os videos de Ubuntu e WSL que já fiz aqui no canal. Tem uma playlist de Software Livre onde falo de assuntos relacionados, caso ainda não tenha assistido e vou adicionar os novos videos nessa mesma playlist. Nos próximos videos vou mostrar um pouco mais em detalhes sobre instalação de algumas distros consideradas mais exóticas, mas hoje quero explorar mais conceitos de Linux e software em geral que talvez vocês não conheçam usando como desculpa a explicação de como um Linux faz boot. Vamos começar a olhar um pouco mais debaixo do capô.</p>
<p>(&hellip;)</p>
<p>Pra escolher distros, hoje em dia é fácil. Basta ir em canais de reviews que tem de tonelada, como do camarada DioLinux ou outros que vem crescendo como do Chris Tut. O comportamento padrão vai ser você vendo qual tem o visual que mais te atrai e daí baixar um Deepin ou PopOS da vida, ou se quiser se sentir mais &ldquo;edgy&rdquo; e &ldquo;gamer hipster&rdquo; da vida, um Garuda. É assim que 99% de todo mundo que usa Linux faz pra escolher.</p>
<p>Além disso, a instalação de qualquer distro tá super trivial. Você baixa o arquivo de ISO no Windows mesmo, usa um programa como o Rufus pra gravar essa ISO num pendrive. Faz boot pelo pendrive e boom, tem um ambiente ao vivo de Linux já funcionando. Ele detecta sozinho praticamente todo o seu hardware. Se nesse ambiente do pendrive parecer que tá tudo funcionando, daí tem um ícone de instalação. Vai abrir o Calamares, que é o programa de instalação que quase toda distro moderna usa. Por isso mesmo meio que tanto faz qual distro escolher, o processo de instalação é muito parecido.</p>
<p>O passo a passo que o Calamares abre é sempre o mesmo. Começa pedindo pra escolher que língua usa e que layout de teclado prefere. Eu sempre escolho inglês e o layout US international que, até hoje, continua com o mesmo problema de não suportar acento agudo no &ldquo;c&rdquo; pra gerar cedilha, mas isso é fácil de consertar com um script que se acha fácil no Google. No caso do Ubuntu, pede pra escolher se quer instalar um monte de aplicativos como LibreOffice, Gimp, aplicativo de fotos e coisas assim, ou uma instalação mínima, que é o que eu prefiro. Eu gosto de já escolher pra baixar os pacotes mais atualizados via internet e também instalar software proprietário de terceiros, como codecs multimídia e drivers de placa de video da NVIDIA.</p>
<p>Se você é iniciante, nem dá bola pra isso, mas no mundo de software livre é uma discussão importante. Por causa de licença de software, toda distro faz um esforço pra ter por padrão somente o que é de verdade software livre, com licenças como GPL ou BSD. Porém, muitos dispositivos, em particular a NVIDIA, não libera o código fonte dos drivers, somente o binário fechado. Mas se você instala isso no seu PC, está concordando com a licença restritiva da NVIDIA e contaminando seu ambiente com software fechado. Do ponto de vista filosófico, não é recomendado. Mas pra nós, usuários finais, meio que tanto faz isso. Por isso sempre habilito, porque eu não sou ativista de software livre. E antes que você vá nos comentários pra dizer que viu notícia que a NVIDIA liberou código dos drivers, nem perca tempo, pra variar é matéria de jornalista bosta que não sabe ler e não viu que eles liberaram só uma pequena parte do código só pra marketing. A parte importante continua fechada e, mais importante, com licença restritiva de uso.</p>
<p>Enfim, o próximo passo costuma ser pra escolher em qual SSD ou HD quer instalar, como quer criar partições e com qual sistema de arquivos. O Calamares costuma ter essa primeira opção que diz o seguinte &ldquo;eu sei que você não tem idéia do que tá fazendo, então deixa que o papai aqui cuida de tudo pra você&rdquo;. É a opção pra idiotas, quero dizer, pessoas não-técnicas. Ele vai apagar o disco todo e criar as partições automaticamente pra você. No caso do Ubuntu, recomendo pelo menos tentar instalar com o sistema de arquivos ZFS em vez do ext4. Se você não tem idéia do que é isso, não esqueça que eu fiz uma minissérie inteira explicando o que são partições, volumes, como o PC faz boot via EFI, e o que diabos é ext4 ou ZFS. Então vai assistir depois.</p>
<p>Finalmente, o Calamares vai mandar preencher um formulário com seu nome e que usuário e senha quer cadastrar pra poder fazer login depois. Obviamente você deveria escolher uma senha aleatória e forte e não &ldquo;senha123&rdquo; ou &ldquo;teste123&rdquo;. Também te dá a opção de criar uma senha diferente pro usuário &ldquo;root&rdquo; que é o administrador da máquina. Mas aqui estou escolhendo a opção preguiçosa que é usar a mesma pros dois usuários e fazer login automático porque só eu vou usar essa máquina mesmo.</p>
<p>E pronto. Acabou. Agora o Calamares vai copiar os arquivos do pendrive pra partição que acabou de criar. Vamos acelerar porque essa é a parte que demora mais, de 15 minutos a meia hora dependendo do seu PC. No final ele configura a carga do boot, e agora é só reiniciar e tá tudo instalado. É absurdamente fácil instalar qualquer distro Linux que usa Calamares hoje em dia. Olha só, vou acelerar aqui o boot, tirar o pendrive, e pronto. Um Ubuntu quentinho recém saído do forno, com tudo funcionando. Se seu PC não for muito bosta, já vai estar tudo detectado, placa de vídeo, som, seu teclado, mouse, wifi e tudo mais. Não precisa fazer mais nada.</p>
<p>Muita gente acha o Ubuntu feio, e eu não discordo muito. Apesar que eles tem melhorado o tema visual e até que hoje, pra mim, não cheira nem fede. Não teria nenhum problema de usar um Ubuntu sem mudar muita coisa. A partir daqui, se você nunca mexeu com Linux antes, veja primeiro o meu video só sobre Ubuntu onde também explico os comandos e conceitos mais básicos que vou assumir que todo mundo já sabe pra continuar este video.</p>
<p>Se escolher um Linux Mint, Elementary, Budgie, Zorin ou PopOS, o funcionamento não vai diferir porque todos são derivados de Ubuntu. Por isso que muito tutorial que funciona no Ubuntu vai funcionar neles também. Cada um tem alguma característica pra se diferenciar. Seja ter um gerenciador de janelas customizado diferente, seja já ter coisas pré-instaladas e pré-configuradas pro usuário não ter que configurar manualmente depois. Mas na prática, pra todos os efeitos e propósitos, é quase a mesma coisa que usar direto Ubuntu.</p>
<p>Antes de continuar, vamos olhar o que fizemos até agora. Um bom programador precisa se acostumar a fazer essas perguntas. Você baixou um arquivo de ISO, gravou num pendrive, e ele logou uma versão light de Ubuntu, sem nem ter instalado nada. Como isso é possível, já parou pra se perguntar isso? Vai, pensa alguns segundos. Como que faz pra bootar um Linux de um pendrive a partir de um mero arquivão com extensão ponto ISO no final?</p>
<p>Se você assistiu minha minissérie sobre armazenamento e sistemas de arquivos talvez já saiba parte da resposta. Deixa eu aproveitar pra mostrar alguns conceitos de Linux pra vocês. Um dos fundamentos inventados no UNIX original e herdado pelo Linux é a idéia de tentar fazer tudo num computador ser representado como se fossem diretórios e arquivos. Por exemplo, talvez já tenha usado o comando <code>ps</code> pra listar os programas rodando. Só <code>ps</code> sem nada mostra os processos que seu usuário iniciou, mas se usar opções como <code>ps aux</code> podemos ver todos os processos que foram iniciados desde o boot pelo usuário <code>root</code> também.</p>
<p>Já expliquei em outros videos sobre processos e como eles tem um PID, que é um process ID, um número que identifica um processo e como podemos mandar sinais sigterm, como <code>kill -9 pid</code> pra matar um processo forçadamente. Agora, lembrem que todo Linux costuma ter um diretório na raíz chamado <code>/proc</code>. Posso fazer <code>ls /proc/pid</code> e olha só, aparece um monte de arquivos e diretórios falsos. O que tem dentro desse diretório não são arquivos de verdade, são informações sobre esse processo representado como arquivos. Por exemplo, posso usar um comando normal de arquivos como o <code>cat</code> que se usa pra listar o conteúdo de um arquivo texto.</p>
<p>Fazendo <code>cat /proc/pid/status</code> temos várias informações sobre esse processo, como quanto de memória ele tá usando. Se eu quiser fazer uma ferramenta parecida com <code>ps, top ou htop</code> da vida, posso consultar as informações de cada processo nessa árvore embaixo de <code>/proc</code>. Fica de exercício pra vocês fazer uma versão parecida com o comando top ou htop, mas usando python ou javascript ou o que quiser, só lendo do diretório /proc, sem usar syscalls pra kernel. Esse diretório não tem arquivos de verdade, só representações virtuais. Quando der boot, esse diretório desaparece. Se você remover esse HD e ligar em outro PC como HD externo USB por exemplo, não vai ter nada dentro.</p>
<p>Esse diretório é o que chamamos de ponto de montagem, um mount point. Tente digitar o comando <code>mount</code> no terminal. Vai aparecer uma lista como essa. Esses são os pontos de montagem ativos atualmente. E que diabos é um ponto de montagem? Num Windows pontos de montagem costumam ser letras como C:, D:, E:. Em Linux são sub-diretórios. Pra explicar isso você precisa entender que, uma coisa é o hardware de armazenamento como um HD ou pendrive. Outra coisa é como o sistema operacional acessa esse hardware.</p>
<p>O que é um dispositivo de armazenamento? Cansei de repetir em vários videos que são dispositivos que gerenciam blocos de bits, ou block devices. Seja um CD, um pendrive USB ou um SSD, todos são dispositivos de bloco. E por acaso, num Linux temos comandos como <code>ls</code> que a essa altura você já sabe que significa <code>listar</code> e <code>blk</code> ou seja listar block devices. E agora sim temos os nomes dos dispositivos plugados no meu PC, por exemplo, o HD principal se chama <code>sda</code>. Um segundo HD se chama <code>sdb</code>, um pendrive, quando plugar vai aparecer como <code>sdc</code> ou <code>sde</code>.</p>
<p>Além de <code>/proc</code> existe outro ponto de montagem que toda distro Linux monta automaticamente no boot, que é o <code>/dev</code>. De novo, o que tem dentro não são diretórios nem arquivos de verdade. Ele representa os dispositivos de armazenamentol. Por isso em muitos tutoriais por aí você já deve ter esbarrado num <code>/dev/sda</code> da vida. Isso não é um arquivo, é um canal pra falar com o HD físico “via” um arquivo virtual. No Windows funciona assim também, o HD principal costuma ter o nome de <code>\\.\PhysicalDisk1</code>. Abre um Powershell no Windows e digita <code>Get-PhysicalDisk</code>. Por exemplo, esse meu NVME Samsung 970 EVO Plus de 2TB se chama PhysicalDisk2. Num Linux um nome equivalente seria <code>/dev/sdc</code>.</p>
<p>Beleza, o Linux tá enxergando os dispositivos, mas agora como navego dentro desses dispositivos? Agora eu preciso de um mount point. O ponto de montagem é justamente dizer que a partição 1 do drive <code>/dev/sda</code> vai ser o diretório raíz &ldquo;/&rdquo; e é pra interpretar os bits desse canal usando o sistema de arquivos ext4. Leia o arquivo &ldquo;/etc/fstab&rdquo; que significa tabela de file system. Em cada linha vai ter dizendo a partição do disco, o ponto de montagem, o sistema de arquivo e opções, por exemplo, pra abrir só pra leitura, caso seja um CD-ROM.</p>
<p>Quando espeto um pendrive nesse meu Ubuntu, automaticamente aparece aqui no meu desktop. Isso porque tem um serviço rodando em background que vai rodar pra mim o comando <code>mount -t vfat /dev/sdc /run/media/akitaonrails/pendrive</code>, por exemplo. Em distros mais antigas, eu precisaria rodar esse comando manualmente no terminal se quisesse acessar o pendrive. E o equivalente a dar um eject é rodar o comando <code>umount /run/media/akitaonrails/pendrive</code>.</p>
<p>Sacaram? Montar é dizer pro sistema operacional como esse dispositivo foi formatado e onde quero que ele apareça e com qual sistema de arquivos é pra interpretar os bits dentro. No Windows isso é feito em aplicativos com o Disk Management, onde posso mudar o ponto de montagem, que no caso é só uma letra de drive, como E:.</p>
<p>Vamos recapitular o que já expliquei nos videos sobre volumes e partições. Num computador moderno, logo que liga, quem vai carregar primeiro é um software chamado firmware, que muitos ainda chamam erroneamente de BIOS. Mas é o software que não está no HD e sim num chip de memória na placa mãe. Por isso que mesmo num PC sem HD nenhum, se você ficar apertando uma tecla como &ldquo;delete&rdquo; várias vezes, entra naquele software de configuração, onde pode atualizar a data e outras configurações do sistema.</p>
<p>Antigamente esse software era chamado BIOS mesmo, que significa Basic Input/Output System, ou sistema básico de entrada e saída que literalmente tem o básico só pra conseguir identificar seu monitor, teclado e mouse pra você conseguir interagir com o computador antes de tentar carregar o sistema operacional do disco. Mas BIOS era um sistema básico até demais, que só conseguia lidar com identificadores de 16-bits, o que impossibilita usar HDs com mais do que 4 partições. Nos anos 90 isso já era pouco e representava um problema.</p>
<p>Por isso hoje todo PC moderno tem um firmware UEFI que significa Universal Extensible Firmware Interface. UEFI é um padrão pra firmwares. Além disso antigamente partições eram organizadas com MBR ou Master Boot Record, que é o registro mestre de boot no HD, que são os bits de boot do sistema operacional que a BIOS vai tentar executar logo na sequência. Hoje em dia é UEFI e GPT que é o GUID Partition Table. Eu falei que antigamente estávamos limitados a 4 partições com identificadores de 16-bits. Em GPT, em vez de números, usamos GUIDs que é uma sequência de 32-bits. Números parecidos com esse aqui que você já deve ter visto. Parece aleatório e não são sequenciais. E com isso hoje podemos ter quantas partições de quantos tipos quisermos. Expliquei tudo isso em mais detalhes no video de tudo que você queria saber sobre dispositivos de armazenamento, depois assiste lá.</p>
<p>Gerenciadores de boot como LILO ou Grub foram criados pra conseguir lidar com as limitações de BIOS e MBR. Com UEFI e GPT nem precisa mais deles, o próprio firmware tem capacidade pra fazer boot de qualquer partição GPT, basta ter o bootloader numa partição que o UEFI consiga ler, formatado em fat32, por exemplo. E isso me faz voltar aqui pro nosso Linux de exemplo. Vamos ver o arquivo &ldquo;/etc/fstab&rdquo; de novo. Olha só, tem uma partição <code>/dev/sda1</code> cujo ponto de montagem é &ldquo;/boot&rdquo;. E de um terminal normal mesmo, usando comandos mundanos como &ldquo;ls&rdquo; podemos ver que tem um diretório chamado <code>/boot/efi</code> e é aqui que fica o bootloader, ou literalmente o carregador de boot que o firmware UEFI vai carregar depois do POST.</p>
<p>É nesse ponto que o firmware UEFI pára de executar e dá controle pro bootloader. O bootloader é quem vai de fato carregar a kernel do sistema operacional, no caso o Linux. E é logo um diretório pra trás, em &ldquo;/boot&rdquo; que está o Linux de verdade. É um binário executável chamado “vmlinuz” até que pequeno, de pouco mais de 10 megabytes, já compilado com os principais drivers que precisa pra iniciar o sistema, como driver pra ler HD e abrir a partição formatada nele, onde vai estar o resto do sistema operacional. O bootloader vai se encarregar de executar o binário e é nesse estágio que a tal kernel do Linux finalmente vai começar a exercer controle sobre a máquina.</p>
<p>A kernel vai começar a carregar drivers pra ter acesso à memória, aos discos, a periféricos como teclados, monitor e tudo mais. Uma vez que agora a kernel tem controle, ela vai criar um espaço em memória com uma partição virtual, usando um initramfs, literalmente file system de ram de inicialização. Sabe seus diretórios de Linux na raíz do seu HD? Pensa uma versão menor disso, mas montado em RAM. Esse sistema de arquivos estava comprimido junto com a kernel naquele arquivo em &ldquo;/boot&rdquo;, é esse outro arquivo chamado “initrd.img”, veja que ele é bem maior com uns 100 megabytes. Mas deixa eu fazer uma tangente pra ficar mais claro.</p>
<p>Vamos abrir o terminal de novo. Existe uma ferramenta que costumamos usar pra fazer clones de HDs ou pendrives no nível dos blocos, independente se tá formatado em ext4, ntfs ou fat32. É o comando &ldquo;dd&rdquo;. E o que significa &ldquo;dd&rdquo;? Significa Copy and Convert .. eh, deveria ser &ldquo;cc&rdquo; só que CC em Linux já significa &ldquo;C Compiler&rdquo;, então escolheram a letra seguinte .. eh, programador é uma bosta pra dar nome pras coisas. Mas enfim, dd funciona assim, tem um parâmetro &ldquo;if&rdquo; que não sei se era &ldquo;input file&rdquo;, é onde coloco o caminho do dispositivo que quero copiar, tipo &ldquo;/dev/sdc&rdquo; e no parâmetro &ldquo;of&rdquo; que talvez seja &ldquo;output file&rdquo; coloco o path de pra onde quero copiar.</p>
<p>Aquele programa de Windows chamado Rufus? É mais ou menos a versão gráfica desse comando. Por exemplo, se do terminal de um Linux eu quiser gravar aquele arquivo de ISO no meu pendrive, poderia ter feito <code>dd if=ubuntu.iso of=/dev/sdc</code> e pronto. Lógico, tenho que garantir que “/dev/sdc” realmente é meu pendrive, porque se eu confundir com uma partição do meu HD, esse comando vai gravar a ISO por cima e já era meus arquivos que tavam lá. Agora, só pra complicar deixa mostrar mais alguns truques de Linux que talvez vocês não saibam. Naquele ponto de montagem &ldquo;/dev&rdquo; que tem representado dispositivos como meus HDs ou pendrives, também temos alguns dispositivos virtuais especiais como &ldquo;/dev/null&rdquo; ou &ldquo;/dev/urandom&rdquo;.</p>
<p>De novo, podemos usar comandos como <code>cat</code> com eles. Vamos fazer <code>cat /dev/null</code> e, uau, nada. Como o próprio nome diz, esse dispositivo devolve nulo. Mas muitos scripts gostam de usar ele pra mandar coisas que quero jogar no lixo. Por exemplo, digamos que eu rode um comando que imprima várias informações na tela que não estou interessado. Posso redirecionar o stdout do comando pra /dev/null pra ele ser engolido e mandado pro limbo. Um exemplo besta é o comando &ldquo;echo&rdquo; que só imprime na tela. Coloco um sinal de &ldquo;maior&rdquo; pra redirecionar pra &ldquo;/dev/null&rdquo; e pronto, engoliu o saída. Se começar a fuçar scripts de shell, vai esbarrar mais nisso.</p>
<p>O &ldquo;/dev/urandom&rdquo; por outro lado é um dispositivo que cospe bits aleatórios. Muitas funções de muitas linguagens, incluindo as funções da kernel do Linux e bibliotecas de criptografia usam esse dispositivo quando precisam gerar números aleatórios. Tecnicamente, isso é um PRNG ou pseudo random number generator. É um gerador de números pseudo aleatórios. Em computador não existe aleatoriedade verdadeira, só uma aproximação. Teríamos que explorar conceitos como entropia e tudo mais pra justificar isso, mas não é objetivo de hoje, só entenda que existe isso.</p>
<p>Pra hoje, mais importante é outro dispositivo virtual, o &ldquo;/dev/zero&rdquo;. Assim como o &ldquo;/dev/null&rdquo; se eu redirecionar a saída stdout de qualquer comando pra ele, vai engolir tudo. Por outro lado se der um <code>cat</code> ou ficar lendo dele, vou receber literalmente nada, só caracteres nulos. Vai cuspindo nulos infinitamente até eu forçar a parada com Ctrl+C. E pra que diabos isso serve? Porque posso usar numa outra ferramenta que aceita dispositivos como parâmetro, como nossa ferramenta &ldquo;dd&rdquo;.</p>
<p>E se em vez de ler de um arquivo de ISO, eu ler desse dispositivo &ldquo;/dev/zero&rdquo;? Vamos voltar ao comando &ldquo;dd&rdquo; e fazer <code>dd if=/dev/zero of=hello.img bs=1M count=10</code> que significa, leia bits do dispositivo &ldquo;/dev/zero&rdquo; e grave num arquivo chamado &ldquo;hello.img&rdquo; usando blocos de 1 megabyte de tamanho e contando até 10, ou seja, até gerar um arquivo de 10 megabytes. Pronto. Criei um arquivo cujo conteúdo é só de zeros. Olha só, <code>ls -la</code> e o arquivo tem mesmo 10 megabytes. Se usar o comando <code>cat</code>, não imprime nada, porque dentro é tudo bits vazios. E mesmo se usar um comando que lê binários como <code>hexdump</code>, ele mostra que os primeiros bits são todos 0 e esse asterisco significa que todo o resto é igualzinho à primeira linha até o endereço 10 megabytes.</p>
<p>E pra que diabos preciso de um arquivo totalmente vazio ocupando espaço com bits zero? Bom, em Linux temos comandos pra formatar partições do seu HD como <code>mkfs.ext4</code> ou <code>mkfs.vfat</code> onde &ldquo;mkfs&rdquo; literalmente quer dizer &ldquo;make file system&rdquo;. Sabe no Windows quando espeta um pendrive, vai no explorar em cima da letra do drive e com o botão direito do mouse escolhe a opção de &ldquo;formatar&rdquo;? É a mesma coisa só que na linha de comando de Linux. Vamos formatar esse arquivo então.</p>
<p>No caso, vou formatar com o <code>mkfs.vfat</code> pra formatar em FAT32, como eu faria com um pendrive que gostaria de conseguir ler independente de que sistema operacional espetar ele. Só fazer <code>mkfs.vfat -F 32 hello.img</code>. Não sei se vocês estão conseguindo entender, normalmente se usa um comando como <code>mkfs.vfat</code> na partição de um dispositivo hardware de verdade, como em &ldquo;/dev/sda1&rdquo;, mas dá pra formatar dentro de um arquivo. E lembra como usando comandos como <code>cat</code> ou mesmo <code>hexdump</code> só voltava vazio? E agora? O que tem nesse arquivo? Vamos ver.</p>
<p>E temos o cabeçalho de uma partição FAT32. Olha só do lado direito o que ele reconhece como caracteres ASCII com mensagens de erro e dizendo que é FAT32. Essa é exatamente a mesma sequência de bytes que encontraríamos lendo direto de um pendrive. Vamos ver? Vamos plugar o pendrive e fazer o mesmo comando de &ldquo;hexdump&rdquo; direto do &ldquo;/dev/sdb1&rdquo; e olha só, esse é o pendrive. São os mesmos tipos de bytes. E já sabemos que a partir de um &ldquo;/dev/sdb1&rdquo; podemos montar num diretório como &ldquo;/run/media&rdquo; da vida. E esse arquivo que acabamos de formatar, dá pra montar também?</p>
<p>Basta criar um diretório qualquer como <code>mkdir hello</code> e fazer <code>mount -t vfat hello.img hello</code>. Podemos dar <code>cd hello</code> e agora estamos DENTRO do arquivo hello.img. Sacaram? Deixa eu criar um arquivo texto qualquer chamado &ldquo;blabla.txt&rdquo; com qualquer coisa dentro e salvar. Vamos listar e olha só o arquivo. Agora vamos sair desse diretório com &ldquo;cd ..&rdquo; e desmontar isso. Só fazer <code>umount hello</code>. Se tentarmos listar o conteúdo do diretório hello agora, não vai ter nada, porque o arquivo blabla ficou dentro do hello.img que acabamos de ejetar, como se fosse um pendrive que desconectamos. O arquivo blabla não foi criado dentro desse diretório. O diretório só tava servindo como ponto de montagem PRA DENTRO do hello.img.</p>
<p>Se eu fizer um hexdump no hello.img olha aqui no fim, os bytes do arquivo que criamos tá dentro dele. Este arquivo hello.img, do ponto de vista do sistema operacional é a mesma coisa que um pendrive, só que sem a parte hardware, só os bits, o linguição de bits. Se ficou confuso, vamos repetir. Lembra o arquivo de ISO que baixamos do site do Ubuntu e gravamos no pendrive? É um arquivão que chamamos de “imagem”. Vamos montar aqui no mesmo diretório hello. Só fazer <code>mount -t iso9660 -o ro ubuntu.iso hello</code>. No caso, um CD-ROM é formatado no padrão iso9660, não é fat nem ext4. E coloquei a opção de &ldquo;ro&rdquo; porque CD-ROM é read-only, só de leitura, não dá pra modificar nada dentro.</p>
<p>Vamos listar o que tem em hello e olha só, todos os arquivos que estão dentro do ISO. É isso que é gravado pelo Rufus num pendrive que queremos dar boot de Ubuntu. Tem o diretório &ldquo;boot&rdquo; com o bootloader, o kernel e tudo mais que precisa pra conseguir bootar. É isso que é um LiveCD. Mas o objetivo de toda essa história é pra vocês começarem a largar noções que existe uma entidade física chamada &ldquo;dispositivo&rdquo;, como um HD ou pendrive, e entender que disposivo de bloco é literalmente qualquer sequência de bits, qualquer linguição de bits, como um arquivo, ou uma conexão de rede. Basta ser formatado de uma forma que dê pra montar com o comando <code>mount</code>.</p>
<p>O sistema operacional não tem preconceitos. Pra ele foda-se se você espetou um pendrive ou se montou um arquivão de imagem. Pra ele só interessa: eu consigo ler e gravar blocos de bits? Então tá, pra mim é um drive. E é assim que consigo montar coisas como um Google Drive ou Dropbox como se fosse um pendrive virtual também. Só que diferente de gravar os bits num arquivo local, o driver de sistema de arquivo do Google Drive vai mandar os bits pela rede pra sua conta no Google. Mas pro sistema operacional, foda-se. Ele mandou blocos, o driver de sistema de arquivos recebeu e disse que gravou. O sistema operacional tá pouco se lixando o que acontece com os bits, ele delega e só espera ouvir um &ldquo;ok, tá gravado&rdquo; e fica feliz.</p>
<p>O que fizemos com a ferramenta &ldquo;dd&rdquo; dá pra fazer o equivalente no Windows com a ferramenta Disk Management. Olha só, temos essa opção de &ldquo;Criar VHD&rdquo; que é virtual hard disk, que é o arquivão vazio. Vamos criar um simples de uns 10 megabytes. Uma vez criado posso anexar esse VHD e tornar ele disponível como um PhysicalDisk numerado. Agora ele aparece aqui embaixo, olha só. Ele vai se comportar como se fosse um pendrive ou HD externo. Eu posso escolher a opção pra formatar como NTFS e no final posso montar com um drive, digamos letra P. Agora o P: é um drive virtual e podemos mover arquivos, criar diretórios, o que quiser. E no final é só desanexar, que é o equivalente de ejetar.</p>
<p>De dentro do Linux posso usar aquele comando hexdump nesse arquivão VHD. E parecido com o arquivão hello.img que criamos com &ldquo;dd&rdquo;, é basicamente um linguição de zeros igualzinho. Olha a linha de zeros e o asterisco embaixo. Mas lá embaixo no arquivo, do endereço a00020 hexa em diante, tem um metadado pra identificar esse arquivão como um VHD. Só por causa desses bytes, que o arquivão que criei com &ldquo;dd&rdquo; não se enquadra como um VHD. Mas na prática é quase a mesma coisa: um linguição de bits zeros.</p>
<p>Voltando pro boot do Linux, parei no ponto onde o bootloader EFI o binário do kernel no diretório “/boot”. Esse arquivo normalmente é chamado de vmlinuz, muitas vezes o nome segue com a versão da kernel, como 5.15. A gente costuma chamar uma distro como Ubuntu ou Fedora de “Linux”, mas na verdade Linux mesmo é só esse um arquivo binário, a kernel. O resto do sistema operacional, o certo é chamar de GNU/Linux, ou seja, ferramentas GNU que rodam em cima de um kernel Linux. Quando o bootloader tem acesso a esse arquivo é quando iniciamos a segunda fase de boot.</p>
<p>Normalmente, no mesmo diretório &ldquo;/boot&rdquo; vai ter um outro arquivo, um &ldquo;initrd.img&rdquo;. Esses nomes vão variar de distro pra distro, mas no caso do Ubuntu e todo mundo que é derivado de Debian, o arquivo vmlinuz e o initrd.img são links simbólicos, tão vendo? O arquivo de verdade é esse que termina com &ldquo;generic&rdquo; no nome. A vantagem de ser link simbólico é porque posso querer deixar kernels mais antigas depois de um upgrade do sistema. Vai que a kernel mais nova veio com algum bug ou incompatibilidade e ferra meu sistema, impedindo bootar?</p>
<p>Não tem problema, se isso acontecer, basta resetar o PC. Vai voltar pro firmware UEFI da placa mãe que, por sua vez vai carregar o bootloader em &ldquo;/boot/efi&rdquo; ou o GRUB, que é aquela tela que tem no começo do boot da maioria das distros Linux. Eu sei que você só dá enter direto sem pensar muito em pra que ela serve. Nessa tela costuma ter uma opção pra justamente poder escolher outra kernel e outras opções de recuperação. Assim dá pra bootar na kernel da versão anterior, que sabemos que funcionava, e ter a oportunidade de consertar alguma coisa. Basta mudar o link simbólico pra kernel e initrd.img anterior e vai voltar a bootar normalmente até sair uma correção pra kernel mais nova. É raro acontecer, mas se acontecer existe essa opção.</p>
<p>Enfim, o arquivo initrd.img é um zipão comprimido, pode ser qualquer formato, a distro que decide. No caso do Ubuntu, eu falei “zipão”, mas não é exatamente um zip como feito pela ferramenta &ldquo;gzip&rdquo; mas sim um arquivo formato CPIO comprimido com LZ4, o Lempel-Ziv 4. Eu expliquei o que é Lempel-Ziv no video &ldquo;De 5 Tera a 25 Giga&rdquo;, depois assiste lá pra saber como compressão funciona. Enfim, o importante é que em Ubuntu podemos usar a ferramenta <code>unmkinitramfs</code> pra descomprimir essa imagem em qualquer lugar.</p>
<p>E olha só, se listarmos o que descomprimiu, vai parecer familiar com a estrutura de diretórios do seu Linux de verdade. Isso é o mínimo que a kernel precisa pra conseguir terminar o boot. Só que aqui descomprimimos essa imagem no nosso HD local. O grande lance é que depois do bootloader conseguir carregar a kernel, ela precisa ter acesso a esses arquivos pra conseguir carregar coisas como driver do seu HD. Por exemplo, e se seu HD tiver partição encriptada com LUKS? Ou se estiver em configuração de RAID dentro de um volume LVM? Como o kernel vai conseguir carregar o driver de RAID pra conseguir montar a partição se já não estiver compilada estaticamente?</p>
<p>São drivers como esse que estão nesse initrd. Só que o initrd em si precisa ser descomprimido em algum lugar. E aqui vou continuar simplificando, mas o que acontece é que a kernel pode não ter acesso ao seu HD no boot. Pode não ter os drivers pra isso. Mas ele tem acesso à RAM. Lembra o lance de criar um um arquivo com o comando &ldquo;dd&rdquo; vazio e formatar pra ser um pendrive virtual? Podemos fazer a mesma coisa na RAM, criar um espaço de bits que vai servir pra dar mount num sistema de arquivos e descomprimir o conteúdo do arquivo initrd pra lá.</p>
<p>Por isso que mesmo se você mandar encriptar sua partição principal ou configurar volumes em RAID, sempre vai ter pelo menos essa pequena partição de boot logo no começo do HD que é formatado em FAT32. Aí o firmware da máquina consegue acesso. Não desperdiça muito espaço, é super pequeno, com não muito mais que uns 100 megabytes. Por isso o initrd é um arquivo compactado, pra caber nessa partição. No Windows mesmo, se você abrir a ferramenta de Disk Management e olhar seu HD, veja, tem essa partição maior onde fica o Windows, e seria o que um BitLocker encriptaria se você escolher.</p>
<p>Mas olha aqui no começo, tem um partição pequena de EFI, que é o equivalente no Linux ao &ldquo;/boot&rdquo; que estamos analisando. Você não enxerga essa partição de boot EFI no Windows porque ele não monta com nenhuma letra de drive, pra não aparecer pros usuários e correr o risco deles estragarem o boot. No caso do Windows, lá no fim do HD tem outra partição escondida sem letra de drive que é a partição de recuperação. Ele é como se fosse o pendrive do Ubuntu que bootamos pra instalar. É o que boota quando você pede pro Windows iniciar em modo de recuperação pra conseguir diagnosticar algum problema que tá bloqueando o boot do Windows principal. É outro Windows, com kernel e drivers separados, em versão reduzida só com ferramentas de diagnóstico e recuperação.</p>
<p>No caso de Linux não temos uma partição assim, porque normalmente usamos pendrive mesmo. Mas é a mesma coisa, a gente gasta um espaço no HD pra ter a comodidade de não ter que sair procurando pendrive se uma hora seu Windows parar de bootar depois de um Windows Update. Já tá tudo no seu HD. Como espaço de HD é barato, podemos desperdiçar algumas centenas de megas. Se você tá pensando em apagar essa partição pra ter um pouco mais de espaço, é hora de pensar em comprar um HD maior, isso sim. Em Macs também tem uma partição escondida com um MacOS reduzido só com ferramentas de manutenção que você consegue acessar se na hora do boot deixar apertado Command + R.</p>
<p>Enfim. Nesse segundo estágio de boot, vamos acabar com um drive virtual em RAM, chamado initramfs ou file system de RAM de inicialização, onde o conteúdo do initrd.img vai ser descomprimido e com isso temos um mini Linux inicializado em memória. Agora sim, a kernel vai ter acesso a drivers extras que precisa e diversas outras ferramentas que pode precisar pra pular pra próxima fase.</p>
<p>Estamos na terceira e última fase do boot. Agora que começa aquela etapa que já viu no boot do seu Linux, quando vai correndo um monte de linhas que eu sei que você nunca parou pra ler. Aquele monte de linhas é o gerenciador de serviços reportando cada um dos serviços que tá conseguindo carregar naquele momento. Esse gerenciador pode ser o famigerado systemd que tem na grande maioria das distros modernas, ou o OpenRC que tem em distros como Slackware ou Gentoo, ou o Runit que acho que é um dos mais antigos, derivados da era dos UNIX originais. Acho que distros baseadas em BSD ainda usam também. Mas seja lá qual for, esse é o primeiro processo que a kernel vai carregar no seu sistema, o famoso processo com ID 1, o PID número 1.</p>
<p>Agora sim, esse sistema de inicialização vai se encarregar de carregar todo o resto dos serviços necessários pra ter um sistema usável. Agora ele consegue acessar a partição principal do seu HD e ler o arquivo &ldquo;/etc/fstab&rdquo;, que é a tabela de file systems onde tá declarado que partição é pra montar como &ldquo;/&rdquo;, a raíz do seu sistema operacional. Ele vai dar mount em tudo que precisa, incluindo os mounts especiais como o /proc, /dev, /sys. Vai carregar os serviços de USB pra achar seu mouse ou sua webcam, serviço de bluetooth que vai conectar com seu fone de ouvido, serviço de network que vai carregar seu wifi, serviço de dhcp client que vai pedir IP pro servidor de DHCP do seu roteador de internet e assim por diante.</p>
<p>Eu zoei que você nunca prestou atenção nesse monte de linhas subindo no boot porque vai rápido demais mesmo. Pra ver em detalhes o que aconteceu, use o comando <code>dmesg</code> depois do boot que lá vai ter essas linhas. Se um serviço deu pau, use o comando `systemctl statusad  e o nome do serviço pra ver se tem alguma dica. Esses serviços tem mais um detalhe na realidade. Logo que a kernel carrega um systemd ou runit, ele tem que ver em qual runlevel vai rodar. Isso também meio que varia, especialmente entre Linux e UNIX ou BSD, mas todos tem conjuntos diferentes de serviços que carregam em runlevels diferentes. Mas, Runlevel? Aí fodeu, deixa eu voltar na história um pouquinho.</p>
<p>Vamos voltar pro boot, a kernel tá carregada, ele carrega seu primeiro programa, de process ID 1, que antigamente era chamado simplemente de &ldquo;init&rdquo;. Esse init ia num diretório como &ldquo;/etc/rc3.d&rdquo; e lá ia achar vários links simbólicos pra scripts que costumavam ficar em &ldquo;/etc/init.d&rdquo;. Então cada sub-diretório &ldquo;rc1.d&rdquo; ou &ldquo;rc3.d&rdquo; podia ter coleções diferentes de links simbólicos de scripts.</p>
<p>Os scripts em si ficavam em &ldquo;/etc/init.d&rdquo;. É o que a gente chama de serviços ou daemons. Eram scripts de shell, que aceitavam parâmetros como &ldquo;start&rdquo; ou &ldquo;stop&rdquo; e dentro configuravam e executavam algum binário, por exemplo, programa e impressora pra montar o spool de impressão, ou o programa X que carrega a interface gráfica e assim por diante. Um daemon é um programa executável normal, com a diferença que tem esse script que inicia ou termina o programa. E o sistema de init executa esses scripts pra inicializar os serviços durante o boot do sistema.</p>
<p>Agora, nesse tal de &ldquo;/etc/rc3.d&rdquo; o “rc” é acrônimo pra &ldquo;run commands&rdquo; ou rode comandos do &ldquo;runlevel&rdquo; ou nível de execução 3. Parece complicado mas originalmente cada runlevel tem uma função específica, o runlevel 0 é pra procedimento de shutdown, o runlevel 6 pra reboot. Runlevel 1 é pra bootar em modo de um usuário só. Runlevel 3 é pra multi-usuário com rede. Runlevel 5 costuma ser pra bootar com interface gráfica.</p>
<p>Se você é de Windows, ele vem com pelo menos 2 runlevels, o do boot normal que cai no Windows e aquele &ldquo;Modo de Segurança&rdquo; ou &ldquo;Safe Mode&rdquo;, lembra? Que boota o Windows sem carregar todos os drivers e serviços, daí você fica sem rede, com resolução baixa, tudo pra quando tiver algum problema no boot, ter um ambiente pra bootar sem carregar quase nada, pra te dar a oportunidade de diagnosticar e consertar o problema. Runlevel é isso, quais conjuntos de serviços vai carregar ou não.</p>
<p>Vamos ver isso na prática. Abrindo o terminal de novo, podemos digitar o comando <code>runlevel</code> e vai dizer que estamos com o runlevel 5 carregado. Digamos que instalamos algum programa gráfico instável que travou tudo. Teclado não funciona, mouse não funciona, nenhuma janela se mexe. Vou simular carregando o Firefox. Faz de conta que foi ele que travou tudo. Nesse ponto a maioria de vocês ia apertar o botão de reset e rebootar. Mas a gente que é de Linux tem outra saída: usar a combinação de teclas Ctrl + Alt + F3 pra mudar pro runlevel 3 que é de multi-usuário com rede em modo texto.</p>
<p>Olha isso, saímos do modo gráfico, que continua rodando no runlevel 5. Agora fazemos login em modo texto e podemos usar comandos como <code>ps aux | grep firefox</code>. Lembra? Na nossa historinha, ele travou tudo, então vamos matar forçadamente com o comando <code>kill -9</code> e o PID dele. Pronto, agora podemos voltar pro modo gráfico. Normalmente eu diria que é só apertar a combinação Ctrl + Alt + F5 mas por alguma razão quando tava escrevendo o script do episódio, isso não funciona, mas Ctrl + Alt + F1 funciona. Enfim, veja como o programa de terminal continua aqui, mas o Firefox morreu. A resolução que zoou aqui, mas deve ser porque tá rodando numa máquina virtual. Esse é um dos jeitos de tentar destravar alguma coisa antes de ser drástico e dar reset na máquina toda.</p>
<p>Dá pra mudar de runlevels no terminal também usando o comando <code>telinit</code> com o número do runlevel. O runlevel 1 na realidade é usado pra manutenção e diagnósticos. E se você mudar pro runlevel 6, isso vai dar reboot na máquina, vamos ver? <code>sudo telinit 6</code> e olha só, rebootou. Sabe quando você usa no terminal comandos como <code>shutdown</code> ou <code>reboot</code>? É isso que ele tá fazendo, mudando pro runlevel 6. E por que isso é importante? Runlevels determinam quais conjuntos de serviços iniciar ou desligar.</p>
<p>Você nunca quer forçadamente apertar reset. Tem vários programas rodando em background, coletando arquivos de log, tem partições montadas que podem ainda não ter feito flush de caches. Você quer notificar todos os programas rodando pra fechar e limpar as coisas antes de realmente desligar a máquina, é o que chamamos de graceful shutdown ou desligamento gracioso. Se ficar dando reset forçado, uma hora vai corromper arquivos ou partições no sistema, por isso nunca force desligar, sempre dê halt, que é mudar pro runlevel 0. Vamos desligar? <code>sudo shutdown -h now</code> ou simplesmente <code>sudo telinit 0</code>.</p>
<p>Na prática, quando o sistema boots, a kernel carrega e inicia um processo 1 como o antigo &ldquo;init&rdquo; ou o atual &ldquo;systemd&rdquo;, ele vai carregar o conjunto de serviços declarados pro runlevel padrão, que costuma ser o runlevel 3 em servidores que não tem interface gráfica ou o runlevel 5 em desktops e PCs como de vocês assistindo, que vai bootar os serviços pro modo gráfico. Na prática significa que você tem diversas formas diferentes de bootar, só que o padrão pra maioria das instalações é entrar no runlevel 5 que é gráfico.</p>
<p>Vamos fazer outro exercício. Digamos que de jeito nenhum consigo entrar no modo gráfico depois de um upgrade ou script mal feito que rodei na máquina, sei lá. Não boota. Nesse ponto a maioria das pessoas já ia pensar &ldquo;que bosta, cadê meu pendrive, vou reinstalar tudo do zero&rdquo;. Calma! No Ubuntu, ele faz como o Windows, tenta ir direto pra tela gráfica. Mas se antes do computador ligar deixarmos a tecla shift apertada, a tela do GRUB vai aparecer.</p>
<p>GRUB é acrônimo pra Grand Unified Bootloader. Como o nome diz, é um bootloader. É ele que o computador vai carregar no boot, e depois se encarregar de achar a kernel do Linux e passar o controle pra ele. O Ubuntu sempre boota com Grub, mas ele carrega quieto, por isso que nunca vemos. Com a tecla shift apertada forçamos a aparecer. Por padrão vai bootar com essa primeira opção aqui, mas podemos apertar a tecla &ldquo;e&rdquo; pra editar os comandos de boot da kernel. Daí vamos pra essa linha aqui embaixo e no final escrevemos o número 3, indicando que queremos bootar no runlevel 3 em vez do padrão que é o 5. Com ctrl + x saímos e vai bootar os serviços do runlevel 3, e olha só, não carrega mais o modo gráfico, podemos logar em modo texto e tentar consertar seja lá o que eu fiz de errado. Daí, no próximo boot vai normalmente pro runlevel padrão que é o 5, de modo gráfico.</p>
<p>Então a sequência é sempre essa: computador liga, carrega o firmware UEFI da placa mãe que vai inicializar o hardware e os dispositivos. Ele vai procurar a primeira partição no HD ou pendrive que tenha o diretório /boot/efi. No nosso caso, dali vai carregar o bootloader do GRUB. O GRUB por sua vez tem uma configuração, que foi o que editamos agora, que diz onde tá a kernel e onde tá a imagem do initrd. Executa a kernel, monta uma partição temporária na RAM chamada initramfs e descomprime a imagem do initrd lá.</p>
<p>Finalmente, a kernel vai carregar um gerenciador de serviços como o systemd como processo número 1 que, por sua vez, vai começar a carregar o resto dos serviços, conforme declarado na runlevel padrão. Cada serviço tem declarado quais outros serviços ele precisa esperar carregar, qual dependência tem, e é isso que vai aparecendo naquele monte de linhas que vai subindo na sua tela no boot. Até finalmente terminar de carregar o servidor Xorg, e o gerenciador de login, no nosso caso, o GDM do GNOME e é aí que aparece a tela gráfica de login.</p>
<p>Esses conceitos de runlevels, diretórios de scripts de serviços em &ldquo;/etc/init.d&rdquo;, links simbólicos pros scripts em diretórios como &ldquo;/etc/rc5.d&rdquo; e o processo chamado &ldquo;init&rdquo; que era o antigo gerenciador de serviços, é o sistema que chamamos System V ou SysV que nasceu nos UNIX originais. Foi assim que eu aprendi em UNIX dos anos 90, e é assim que gerenciadores como o Runit e acho que o OpenRC ainda funcionam. Mas em distros modernas, nada disso vale mais, pode esquecer tudo que eu falei.</p>
<p>Distros como Ubuntu, Fedora, OpenSuse, Manjaro e muitos outros, especialmente se usam gerenciador de janelas GNOME, aderiram ao SystemD e ele descarta todos esses conceitos. Não existe runlevels em systemd e sim targets. E apesar de ter diretórios como &ldquo;rc3&rdquo; ou &ldquo;rc5&rdquo; eles não são mais usados. Em vez disso existem diretórios como &ldquo;graphical.target&rdquo; ou &ldquo;multi-user.target&rdquo;. E eles tão espalhados em diversos diretórios como &ldquo;/etc/systemd/system&rdquo; ou &ldquo;/usr/lib/systemd/system&rdquo; e assim vai. Ele tenta simular o comportamento dos runlevels pra manter compatibilidade com comandos como <code>telinit</code> que usamos antes.</p>
<p>É uma das razões de porque tanta gente odeia o systemd e você vai achar fios de reddit e grupos em discord de gente xingando. Eu mesmo não entendo o systemd a fundo, nem de perto. Ele faz bem mais que o Init de SysV antigo e tenta fazer muito mais coisas por baixo dos panos. Eu me acostumei a usar, mas quem desenvolve pra Linux deve ter mais motivos técnicos de porque isso incomoda. Só o fato de mudar conceitos tradicionais com runlevels pra targets, deve dar um trabalho extra pra manter software pra diferentes distros que não aderiram a esse novo padrão.</p>
<p>Por exemplo, em &ldquo;/usr/lib/systemd/system&rdquo; tem esses links simbólicos que tenta equiparar o que seriam os antigos runlevel numerados com os atuais targets. Veja, o runlevel5 é um link simbólico pra graphical.target. O runlevel6 é link pro reboot.target. Vamos ver o que é o graphical.target e olha só, é uma configuração que fala que requer carregar a mesma coisa que o multi-user.target, o antigo runlevel 3, mas também quer carregar o display-manager.service. Faz sentido, o modo gráfico nada mais é do que o modo multi-usuário que carrega em texto, mas carregando o serviço de tela pra mostrar interface gráfica.</p>
<p>No terminal, com o comando &ldquo;runlevel&rdquo; ainda consigo ver os runlevels rodando agora, e com o novo comando <code>systemctl get-default</code> consigo ver que o target padrão de boot é o graphical.target. Se quiser mudar pra das próximas vezes bootar em modo texto, posso usar o comando <code>systemctl isolate multi-user.target</code>, que é mais ou menos o equivalente antigo a usar o comando &ldquo;telinit&rdquo;. Cuidado ao fazer isso porque ele vai rebootar o sistema, mas não vai mudar o target padrão, que ainda vai ser graphical.target.</p>
<p>Pra todas as vezes bootar só em modo texto, posso mudar o target padrão fazendo <code>systemctl set-default multi-user.target</code>. Esse comando &ldquo;systemctl&rdquo; você vai ver em diversos tutoriais. Quando instala Docker por exemplo, o tutorial costuma mandar habilitar o serviço pra carregar em todo boot fazendo <code>systemctl enable docker</code>, que vai adicionar no target de graphical.target. E pra carregar o serviço na hora pode fazer <code>systemctl start docker</code>. Pra ver se o serviço tá carregado, basta fazer <code>systemctl status docker</code>, e pra ver todos os serviços que estão carregados agora é só rodar <code>systemctl --type service</code>.</p>
<p>A maioria dos tutoriais hoje assume que você está usando uma distro com systemd e por isso sempre vai pedir pra rodar comandos como <code>systemctl start</code>. O systemd em si faz bem mais coisas que iniciar ou rebootar serviços, então recomendo que leia sobre ele em wikis como do site do Arch ou Gentoo, que costumam ter exemplos de como usar e mais detalhes do funcionamento. Como disse antes, eu mesmo não sei todos os detalhes dele.</p>
<p>Enfim, o objetivo de hoje foi juntar o antigo video de Ubuntu e um pouco dos videos de WSL2 com a minissérie sobre armazenamento. Recomendo que assistam agora se ainda não viram. Pra continuar a minissérie, no próximo episódio quero explicar mais conceitos de Linux mostrando como uma instalação realmente funciona, então se ficaram com dúvidas, mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal pra não perder o próximo episódio e compartilhe o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>linux</category><category>tutoriais</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #123 - Como sua Internet Funciona | Introdução a Redes Parte 3</title><link>https://akitaonrails.github.io/2022/07/23/akitando-123-como-sua-internet-funciona-introducao-a-redes-parte-3/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/07/23/akitando-123-como-sua-internet-funciona-introducao-a-redes-parte-3/</guid><pubDate>Sat, 23 Jul 2022 11:54:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/gcv5hXyTcIo"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Vamos mais na prática como a internet da sua casa funciona. O que vem depois do modem. Como seu PC, seu celular, sua TV, conseguem se comunicar e acessar a internet? E se no ipv4 dizem que já acabou os possíveis endereços de IP, como continuamos usando internet sem migrar pro tal ipv6? Aliás, o que é ipv6? Vamos tirar essas coisas básicas do caminho.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/gcv5hXyTcIo"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Vamos mais na prática como a internet da sua casa funciona. O que vem depois do modem. Como seu PC, seu celular, sua TV, conseguem se comunicar e acessar a internet? E se no ipv4 dizem que já acabou os possíveis endereços de IP, como continuamos usando internet sem migrar pro tal ipv6? Aliás, o que é ipv6? Vamos tirar essas coisas básicas do caminho.</p>
<h2>Conteúdo:<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Introdução</li>
<li>00:47 - Tanenbaum</li>
<li>01:43 - OSI Model</li>
<li>04:39 - Configurando roteador</li>
<li>06:22 - Endereço ipv4</li>
<li>07:36 - Máscara de Rede</li>
<li>09:23 - Telefones e ramais</li>
<li>11:28 - NAT</li>
<li>14:25 - ARP</li>
<li>16:40 - Default Gateway</li>
<li>17:56 - DHCP</li>
<li>19:39 - Hubs e Switches</li>
<li>21:58 - DNS</li>
<li>26:30 - Pi-Hole (DoH)</li>
<li>27:57 - Roteamento</li>
<li>30:20 - CGNAT</li>
<li>33:20 - IPv6</li>
<li>40:23 - Conclusão</li>
<li>40:53 - Bloopers</li>
</ul>
<h2>Links:<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Computer Networks (5th Edition PDF) (<a href="https://www.mbit.edu.in/wp-content/uploads/2020/05/Computer-Networks-5th-Edition.pdf"target="_blank" rel="noopener">https://www.mbit.edu.in/wp-content/uploads/2020/05/Computer-Networks-5th-Edition.pdf</a>)</li>
<li>What is a DNS record? (<a href="https://www.cloudflare.com/learning/dns/dns-records/"target="_blank" rel="noopener">https://www.cloudflare.com/learning/dns/dns-records/</a>)</li>
<li>MAC spoofing (<a href="https://en.wikipedia.org/wiki/MAC_spoofing"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/MAC_spoofing</a>)</li>
<li>What is Carrier-grade NAT (CGN/CGNAT)? (<a href="https://www.a10networks.com/glossary/what-is-carrier-grade-nat-cgn-cgnat/"target="_blank" rel="noopener">https://www.a10networks.com/glossary/what-is-carrier-grade-nat-cgn-cgnat/</a>)</li>
<li>What is IPv6, and why is adoption taking so long? (<a href="https://www.networkworld.com/article/3254575/what-is-ipv6-and-why-aren-t-we-there-yet.html"target="_blank" rel="noopener">https://www.networkworld.com/article/3254575/what-is-ipv6-and-why-aren-t-we-there-yet.html</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Finalmente vou poder falar um pouquinho mais sobre a parte que mais interessa pra programadores iniciantes, que é o básico do básico de uma rede caseira. Eu resolvi falar disso porque recentemente estava justamente fuçando minha própria rede de casa, configurando meu NAS novo, e pareceu uma boa hora pra introduzir alguns conceitos de rede. Muito do que quero falar em episódios futuros depende de todo mundo saber pelo menos esse básico.</p>
<p>Hoje pulamos do modem pro roteador e o vocabulário mais básico sobre endereços IP, rotas, e tudo mais. Pra maioria das pessoas tudo isso é invisível. Você já conecta no seu Wifi e tudo funciona. Mas programadores precisam entender porquê essas coisas funcionam. Então vamos lá.</p>
<p>(&hellip;)</p>
<p>Sem vocês saberem nos últimos dois episódios eu toquei em dois tópicos específicos de redes. O que se chama de camada física e a camada de dados, physical layer e data link layer. Claro, eu só toquei em pouquíssimos tópicos desses assuntos, se quiserem saber o assunto completo, como sempre, eu recomendo que estudem o famoso livro de redes do professor Tanenbaum. Quem fez ciências da computação provavelmente teve que estudar esse livro, e na minha época da faculdade também era ele.</p>
<p>Se conseguiram chegar até aqui, espero que já consigam enxergar dados não como arquivos e sim como conjuntos de blocos de bits, ou o que chamamos de pacotes. E espero que já tenham começado a entender a idéia de protocolos como o formato como preenchemos os metadados do cabeçalho desses pacotes. A metáfora de e-commerce onde o produto é o dado e o pacote de fora onde se preenche endereços é o protocolo. Esses pacotes tem coisas como o número deles pra depois a gente receber e montar na ordem certa.</p>
<p>O hello world do estudo de redes é o modelo de referência OSI, que tecnicamente não é mais usado hoje em dia. A gente tá mais familiarizado direto com o protocolo TCP/IP que se usa na internet. O Tanenbaum diz que os protocolos associados ao modelo OSI não são mais usados, mas o modelo em si é bem útil e geral e ainda válido. Já no TCP/IP é o contrário, o modelo em si não é muito útil mas os protocolos são muito utilizados. Por isso na prática a gente usa TCP/IP mas aprende o modelo OSI.</p>
<p>O modelo OSI é dividido em 7 camadas e eu disse que já mencionei parte das duas mais baixas, a camada física 1 e a camada de data link 2. A idéia das camadas é que elas poderiam ser substituídas sem alterar as camadas de cima. Por exemplo, posso trocar a camada física de cabos ethernet por wifi e o resto funciona igual. A camada física é isso, a camada responsável por transportar bits por algum canal de comunicação.</p>
<p>A camada de data layer é responsável por cuidar de coisas como checagens de erros. Checksums e o algoritmo Internet 16-bits que mencionei no episódio anterior. A idéia de detectar um erro e pedir pra repetir o pacote caso tenha erro. Depois vem a camada que não falei, a camada de rede propriamente dita, é quem cuida das rotas e pra onde enviar os pacotes.</p>
<p>No caso de TCP/IP as camadas física e data link são uma camada só, a camada chamada só de link. E a camada de rede é a responsabilidade do protocolo IP. Quando falamos em IP pensamos em endereço IP, mas IP em si é Internet Protocol, que é a camada de Internet junto com outro protocolo, o ICMP ou Internet Control Message Protocol, que a gente usa quando fazemos ping por exemplo.</p>
<p>Acima disso, no modelo OSI temos camada de transporte, sessão, apresentação e de aplicação. Tecnicamente a camada de transporte em si é o TCP que é Transport Control Protocol, ou protocolo de controle de transporte. Portanto TCP/IP é justamente protocolo de controle de transporte de pacotes na internet. TCP tem como característica controlar conexão entre duas pontas de forma confiável. Em paralelo temos UDP ou User Datagram Protocol, que é um protocolo que não fecha conexão e não usa o sistema de sequenciamento de pacotes do TCP. Ele tem menos metadados e é mais leve, mas é menos confiável e feito pra situações onde perder um pacote não é um problema. Streaming de audio e video é um exemplo onde UDP pode ser útil.</p>
<p>A camada de aplicação é onde encapsulamos outros protocolos dentro. É onde nós programadores mais trabalhamos com coisas como HTTP pra web, SMTP pra email, DNS pra domínio e assim por diante. A maioria dos protocolos que usamos no dia a dia de programação são empacotados nessa última camada de aplicação. Pras camadas embaixo, não importa o que foi encapsulado, porque vamos fragmentar tudo em frames e eles só se importam em entregar esses frames de uma máquina pra outra. É como um entregador do mercado livre. Pra ele não interessa o que tem dentro do pacote, ele vai entregar do mesmo jeito seja lá o que tiver lá dentro.</p>
<p>Mas entregar, precisamos saber pra quem entregar. Eu disse no episódio de modem que a responsabilidade dele é modular e demodular entre sinal digital e sinal analógico. Mas essas caixinhas de uma Vivo ou Claro da vida fazem mais, são também roteador e ponto de acesso. É como um negócio pequeno familiar como a padaria do pai que é padeiro, a mãe recebe pedidos, manda o filho ir entregar e vai funcionando assim. Essa é a beleza dos protocolos de internet, você pode entuchar tudo num PCzinho fraco que vai funcionar ou dividir responsabilidades com PCs mais fortes pra escalar. Pra maioria das pessoas, com 1 TV, 3 ou 4 celulares, 1 ou 2 PCs, usar essa caixinha tá mais que bom e vai funcionar bem o suficiente que poucos vão notar a perda de performance e possível falta de segurança.</p>
<p>Eu mesmo não tenho usos muitos drásticos, mas talvez por ser mais old school, eu prefiro desligar a função de ponto de acesso e deixar ele conversar só com um dispositivo, o meu router separado. Não é um router robusto nem nada disso, é só um TP-Link Archer C6 normal. Eu uso um cabo cat5e que suporta até 1 gigabit. Como meu plano é de 300 megabits só, mais do que isso sobra. O roteador registra o gateway de saída padrão como sendo o IP do modem que é 192.168.15.1. E o roteador como sendo o dispositivo 192.168.15.2.</p>
<p>Hoje vamos falar desse tal de endereço IP, esse número de quatro blocos divididos por pontos que todo mundo já viu pelo menos uma vez na vida. Infelizmente pra realmente explicar isso eu precisaria tocar no assunto de roteamento, classless interdomain routing ou CIDR, e eu realmente não tenho nenhuma vontade de explicar isso. Então, de novo, recomendo o livro do Tanenbaum e o capítulo sobre a camada de rede pra estudar. Dessa vez só vou dizer o que é, mas não porque é assim.</p>
<p>Mesmo cortando o grosso, ainda assim explicar endereços é um saco. Vamos por partes. Um endereço IP é basicamente um número de 32-bits que escolhemos dividir em 4 blocos de 8 bits. Assim como em decimal, quando temos um número grande como 1 milhão, a gente costuma separar por ponto a cada três números pra ficar mais fácil de ler. Sendo blocos de 8 bits, se você é de front-end já tá acostumado com cores RGB sendo 3 blocos de 8 bits cada, indo de 0 até 255. Mesma coisa. Os endereços IP podem ir de 0.0.0.0 até 255.255.255.255.</p>
<p>Aqui vale falar outra coisa, eu venho falando em blocos ou pacotes ou frames, mas existem várias nomenclaturas usadas de formas confusas e no mundo de IP fala-se em datagrams, é a mesma coisa, pacotes de bits com um cabeçalho de metadados. Assim como antigamente tinha telegrama no mundo analógico, em digital temos datagramas. Mas no fundo é tudo pacote de bits, não se perca nas diferentes nomenclaturas. A parte importante são os campos de metadados definidos pelo protocolo, que descrevem pros diversos componentes da rede, como switches e seus dispositivos o que fazer com esse pacote, pra onde mandar e tudo mais. Mas realmente não tenho nenhuma vontade de detalhar esses campos, novamente, leia Tanenbaum.</p>
<p>Mas voltando, endereços IP versão 4 ou IPv4 como chamamos são números de 32-bits. Mas só esse número não quer dizer muita coisa sozinho. A gente sempre precisa de uma máscara de sub-rede, subnet mask, que é aquele número 255.255.255.0 ou similar que você também já deve ter visto e como o próprio nome diz é uma máscara pra ser aplicada no endereço IP pra saber qual parte define a sub-rede e qual define o host. Isso é uma coisa que todo programador tem obrigação de saber que são operações bitwise.</p>
<p>Se nunca viu operação bitwise, vamos ver como que funciona. Vamos pegar o endereço que o modem deu pro roteador, 192.168.15.2 e escrever o número em binário aqui na tela, 32 bits. Eu quero saber qual é o prefixo que identifica a sub-rede. Pra isso usamos a máscara. Lembra, 255 significa tudo 1, então 3 blocos de 8 bits 1 e o resto é zero. Agora fazemos operação bitwise AND, ou seja, fazemos AND bit a bit. 1 and 1 é 1, 1 and 0 é 0, 0 and 0 é 0. Eu expliquei isso no guia mais hardcore de introdução a computadores.</p>
<p>Fazendo isso, o que sobra no final é o prefixo de rede. 192.168.15.0. A máscara arranca fora o sufixo que identifica o dispositivo individual na rede. Essa máscara na prática me diz que os primeiros 24 bits representam a rede, por isso outra forma de representar isso é escrever 192.168.15.255/24. Isso denota uma rede onde é possível ter até 256 dispositivos. Se fosse 192.168.255.255/16 ou máscara 255.255.0.0 significa que poderíamos ter 2 elevado a 16 ou seja, mais de 65 mil dispositivos. É uma rede grande demais, por isso você não vê rede barra 16 por aí. Num roteador caseiro, uma rede barra 24 é mais que suficiente, porque ninguém tem mais de 200 dispositivos em casa precisando de endereços.</p>
<p>Por que precisa ter isso? Um roteador é responsável por rotear pacotes na rede interna e também ligar com outro roteador numa rede externa, tipo a rede da Vivo. Se não tivesse essa hierarquia de rede e hosts, todo roteador precisaria ter uma tabela com cada endereço de cada dispositivo dizendo pra onde o pacote de cada um deles precisaria ir. Em vez disso o roteador só precisa saber, se da rede internet vier um pacote cujo prefixo do endereço seja 192.168.15.x ele precisa mandar pra rede externa do modem. Não importa se veio de 192.168.15.2 ou 192.168.15.3. Então o roteador só precisa manter uma linha em memória pra essa rede em vez de uma tabela com todos os endereços dessa rede.</p>
<p>É mais ou menos como telefones. Se todo mundo tivesse um número de telefone de 11 números aleatórios, toda central em todo estado precisaria ter uma tabelona com 100% de todos os números pra saber pra onde mandar. Mas a gente usa o tal de DDD, os dois primeiros números representam uma região, mais ou menos uma cidade ou conjunto de cidades. Na Grande São Paulo o DDD é 11, Rio de Janeiro, o DDD é 21, em Natal o DDD é 84 e assim por diante. Então se a Central de São Paulo faz uma ligação pra um número com prefixo 84, ele sabe que precisa rotear a chamada pra Central da Natal. A Central de São Paulo não precisa ter o banco de dados com os números de Natal, só mandar pra outra central que vai ter esse sub-conjunto de números de Natal, que é a Central de Natal.</p>
<p>Endereços IP é a mesma coisa, só que mais simples, ele representa a sub-rede e o host, ou identificador do dispositivo nessa rede. Mas tem mais detalhes pra quem quer estudar redes de verdade que é entender CIDR ou Classless InterDomain Routing, como se usa máscaras pra definir sub-redes independentes dentro de uma mesma organização, por exemplo, blocos de IPs diferentes pra cada departamento. E a forma antiga que se usava até 1993 de classful addressing, onde provedores tinham que lidar com classes A até E. Como falei antes, isso tudo tem a ver sobre como roteadores sabem como rotear quais pacotes pra quais redes. Isso é o capítulo de camada de rede do Tanenbaum.</p>
<p>O que mais interessa é o problema que vocês já devem ter ouvido falar. Especialmente depois da explosão da internet no fim dos anos 90, seguido da explosão de smartphones nos anos 2010, a gente literalmente tem ordens de grandeza mais dispositivos conectados na internet do que existem endereços disponíveis num número de 32-bits como definido pelo IPv4. É mais ou menos o mesmo problema que expliquei no episódio dos 640 kilobytes de RAM quando se achava que isso era o máximo de RAM que uma pessoa normal jamais ia precisar, e nos anos 90 se provou que isso era muito pouco. 32-bits que dá um máximo de 4 gigabytes também é pouco e hoje estamos em 64-bits.</p>
<p>IPv4 sendo 32-bits significa que o máximo teórico de endereços seria pouco mais de 4 bilhões. Só de smartphones já temos mais de 3 bilhões. Some a isso todo PC, todo servidor em todo data center. Some a isso um tanto de IPs que já foram desperdiçados também. A gente já passou de 4 bilhões de endereços muitos anos atrás. Mas porque tudo continua funcionando? E pra entender isso eu acho importante todo mundo saber o que é um NAT ou Network Address Translator.</p>
<p>O conceito é muito simples. Todo provedor como Vivo ou Claro tem muito menos blocos de endereços IP reais do que clientes. Por isso é impossível dar um endereço real pra cada cliente e de fato ninguém faz isso. Em vez disso eles implementam o que chamamos de um CGNAT ou Carrier Grade NAT, ou seja, um NAT tamanho industrial. O grande lance é que podemos ter o intervalo inteiro de IPv4 dentro de cada sub-rede isolada e traduzir o endereço quando ela sai de uma rede e vai pra outra.</p>
<p>Em redes embaixo de NAT existem 3 blocos de endereços reservados só pra isso. Os com prefixo 10 barra 8, que é um intervalo de 24 bits de mais de 16 milhões de endereços possíveis. Ou com prefixo 172.16 que é barra 12, uma rede de 20 bits ou mais de 1 milhão de endereços e finalmente com prefixo 192.168 que é barra 16 que é uma rede de 16-bits ou mais de 65 mil hosts. Por isso que normalmente seus dispositivos na sua rede local de casa ou do escritório costumam ser 192.168 alguma coisa ou 172 ponto alguma coisa ou 10 ponto alguma coisa. São intervalos reservados pra redes locais embaixo de NAT.</p>
<p>Meu PC tem endereço 192.168.1.200. Eu quero assistir alguma coisa do YouTube. Faz de conta que o endereço de destino é 142.250.219.14 porta 443 que é a porta de HTTPS. Outro dia explico portas. Então meu PC monta pacotes onde a origem é meu endereço 192.168 com destino 142.250 bla bla porta 443 pedindo a página de video e manda isso na rede local aqui de casa.</p>
<p>Antes de chegar em NAT vou aproveitar pra adicionar mais detalhes desse processo. Dentro de uma rede local os pacotes não são roteados pelo endereço IP. No protocolo de rede, estamos na camada de rede, mas na rede local o que importa é a camada de data link, da ethernet. E ethernet não entende endereços IP.</p>
<p>Essa tangente vale a pena fazer. Segura o exemplo do YouTube. Digamos que eu queira falar com um dispositivo aqui dentro na minha rede local. No caso o meu NAS. Eu que configurei então sei que tá com endereço local 192.168.1.161. Posso abrir aqui o command prompt do Windows e dar um ping pra ele. Todo mundo já fez ping pra medir latência e tempo de resposta, ele manda um pacote ICMP e mede o tempo. De novo, como meu PC sabe mandar pacote exatamente pra esse dispositivo e não confunde com, digamos, minha smart TV que tá na mesma rede?</p>
<p>O jeito mais simples seria toda vez eu ficar mandando pacotes pra todo mundo na rede perguntando &ldquo;ow, quem aí é IP final 161&rdquo;, daí o NAS responde &ldquo;sou eu! manda pra cá&rdquo;. E ficar fazendo isso toooooda vez, pra todo pacote. Mas conseguem enxergar como isso seria um desperdício? Eu ia ficar toda hora incomodando os vizinhos gritando no corredor. Minha TV ia receber pacotes que não precisa. Meu smartphone conectado via Wifi ia ficar recebendo. Toda vez que preciso enviar pacotes, ia ter que ficar gritando no corredor. Seria um saco.</p>
<p>Por isso existe um outro tipo de pacote chamado ARP ou Address Resolution Protocol. No command prompt mesmo eu posso digitar <code>arp -a</code> e vai aparecer uma tabela, que é um cache aqui no meu PC. Quando eu fiz o ping pro meu NAS, na realidade ele pediu pro ARP gritar no corredor &ldquo;ow, quem aí é o NAS!?&rdquo;. Aí o NAS respondeu &ldquo;sou eu, caralho!&rdquo; e incomodou todo mundo na rede. Mas aí o ARP que não é burro nem nada, registrou a posição do apartamento no andar. É pra isso que serve o tal MAC address. Toda placa de rede, seja ethernet, seja wifi, tem um endereço único de 48 bits que vem de fábrica. 2 elevado a 48 são mais de 281 trilhões de endereços. É endereço suficiente pra todas placas de rede já fabricadas e em operação.</p>
<p>Olha a tabela que o comando <code>arp -a</code> me mostra agora. Tem um registro dizendo que 192.168.1.161 é esse MAC address que eu escondi uma parte. O endereço IP é relevante pra roteador de internet, mas na camada de baixo da rede física, IP não quer dizer nada. Ethernet encontra coisas baseado em MAC address. Mas com isso meu PC já sabe que se quiser falar com o NAS de novo, não precisa mais gritar no corredor. É só mandar direto pra placa de rede que responde nesse MAC address.</p>
<p>Mas isso funciona pra endereços da rede local. Se for endereço IP 192.168 o ARP vai ficar gritando aqui no corredor. Mas e se for o endereço do YouTube, que é no prédio ali na esquina? Agora não adianta ficar gritando no corredor. E é pra isso que serve o tal do default gateway que você também costuma ver na configuração de rede do seu roteador. É como se fosse o porteiro do seu prédio. Todo endereço que não faz parte deste prédio você manda pro default gateway, que por convenção é o endereço IP mais baixo, como 192.168.1.1. E agora pede pra ele ir no outro prédio, pedir pro porteiro de lá, o roteador de lá, gritar no corredor de lá. Rede é mais ou menos isso mesmo, todo mundo gritando pra lá e pra cá e uma correria de entrega de pacotes.</p>
<p>Mas tudo isso pressupõe que todo apartamento tem um endereço IP único dentro do prédio. Mas digamos que se mudou um vizinho pentelho, e só pra foder, ele resolveu colocar o mesmo número de apartamento do vizinho. O ARP vai gritar no corredor &ldquo;ow. quem aí é endereço final 161&rdquo; e dois apartamentos gritam &ldquo;sou eu!!&rdquo; e agora? Agora eu não posso entregar o pacote pra nenhum dos dois, porque não sei dizer quem tá dizendo a verdade. Então o pacote é dropado, jogado fora, e nenhum dos dois recebem. Um deles precisa colocar um endereço IP que nenhum outro esteja usando. Essa é a regra de conduta do prédio.</p>
<p>Pensa, seria uma zona se toda vez que você se muda pra um prédio, você mesmo precisasse definir qual é o número do seu apartamento. O prédio é progressista, ele deixa você escolher o número que mais te agrada. Só que aí metade escolhe número 13, a outra metade escolhe 22. E o ARP ia ficar o dia inteiro gritando nos corredores e não ia poder entregar nada pra ninguém. O prédio seria uma zona. Ainda bem que na vida real, todo apartamento já tem números pré determinados e você não escolhe. Hoje em dia, numa rede local, é a mesma coisa.</p>
<p>Pra isso serve essa outra coisa que você já deve ter visto no seu roteador chamado DHCP ou Dynamic Host Configuration Protocol. Toda vez que um novo dispositivo se conecta na rede, ele pergunta pro servidor de DHCP, que em cenário caseiro, fica no seu modem ou no seu roteador separado. Em empresas um servidor DHCP pode ser um servidor separado e independente. Mas o funcionamento é o mesmo. O DHCP é o síndico do prédio. O meu PC conecta na rede e checa &ldquo;ow, tem um síndico ativo por aí?&rdquo; e o DHCP responde &ldquo;opa. tem eu aqui! o que manda?&rdquo;. Aí o PC pergunta &ldquo;ow, que endereço tem sobrando aí pra mim?&rdquo; e o DHCP mantém uma tabela parecida com aquela do ARP, e fala &ldquo;opa, tem final 200, firmeza?&rdquo; e o PC responde &ldquo;valeu irmão, vou usar esse então&rdquo;.</p>
<p>E quando o NAS conecta, ou a TV ou seu smartphone, todos perguntam pro DHCP qual endereço tá sobrando e ele manda um certinho, de tal forma que ninguém tem um IP colidindo com de outro dispositivo. E assim o ARP funciona e os pacotes são entregues pros lugares certos. No DHCP eu posso também configurar dizendo que um certo MAC address sempre receba o mesmo endereço de IP. É útil pra coisas como meu NAS porque eu monto um drive nele do meu PC e seria um saco se toda hora o DHCP desse um endereço diferente pra ele.</p>
<p>Tudo parece encaixar bonitinho, mas tem alguns problemas que eram mais graves no começo da internet e foram sendo tratados ao longo do tempo. Por exemplo, antigamente a gente usava hubs em redes, pra compartilhar um cabo entre vários computadores. Lembra antigamente quando tinha extensões de telefone na sua casa? Aí seu namorado liga e toca nos dois telefones, a menina no quarto atende e a mãe na cozinha atende também. Daí a mãe se toca &ldquo;opa, é o namorado da minha filha, vou desligar pra não ouvir a conversa&rdquo;. E sendo uma mãe honesta, ela desliga. Mas isso pressupõe que todo participante nessa rede seja honesto.</p>
<p>É o que placas de rede legítimas numa rede em hub fazem. Se vem pacotes que não é pro MAC address dele, ele dropa, ou rejeita esses pacotes e não fica escutando. Porém, a placa de rede tá aqui fisicamente na minha máquina, eu posso hackear ela. E isso se chama colocar essa placa em modo promíscuo. Quem já brincou com softwares como Wireshark sabe disso. A gente podia ficar vendo todos os pacotes trafegando na rede, mesmo os que não são pra mim. Por isso que hoje em dia se usa switches. Um switch estabelece uma conexão direta entre o dispositivo e o roteador e os outros canais do switch não vêem pacotes que não sejam direto pro MAC address dele. Por isso que hubs são baratinhos, porque eles enviam os pacotes pra todo mundo conectado, sem processar nada.</p>
<p>Mas ainda tem outro problema. De novo, MAC address é um endereço que vem de fábrica na placa de rede. Mas eu posso modificar essa placa e fazer ele responder qualquer outro MAC address. Daí o switch é obrigado a mandar pacotes pra mim que não eram pra mim. Não só isso, mesmo meu endereço IP mudando por causa do DHCP, meu MAC address é sempre o mesmo, então o provedor consegue saber o tempo todo com quem estou me conectando e registrar isso. Por isso empresas como a Apple vem planejando gerar MAC address aleatórios em iPhones por exemplo, pra dificultar saber quem é quem.</p>
<p>Na prática, se você compartilha a internet numa casa, dormitório ou sei lá, com outras pessoas, não use hubs, use switches. Não importa se você confia nas pessoas da sua casa, você não sabe se ela sem querer instalou um malware que colocou o PC dela em modo promíscuo e tá escutando os pacotes da sua rede. Em computação você não deve nunca confiar em ninguém, sob nenhuma hipótese, essa é a regra número um. Nada nunca é 100% confiável, então assuma sempre 100% suspeito.</p>
<p>Enfim, com esses componentes básicos você já deve entender que seus dispositivos conseguem endereços IP usando um servidor de DHCP e por isso muitos de vocês nunca precisaram configurar endereços na mão. Também já sabem que tem esse ARP que descobre qual dispositivo na rede responde pra qual endereço e mantém um cache que faz o de-para de endereço IP pra MAC address. Sabe que todo mundo está conectado no roteador seja diretamente via cabo de rede, via wifi, e se for cabo e tiver múltiplos dispositivos, coloca um switch pra conectar todo mundo de forma mais segura.</p>
<p>E sabe que internamente os pacotes são roteados pelo MAC address, mas se eu quiser acessar servidores fora da minha rede, peço pro default gateway, que é tipo o porteiro do prédio, pra ir falar com o porteiro do outro prédio. E esse é o básico do básico de redes que você precisa saber.</p>
<p>Mas vamos voltar pro exemplo de acessar o YouTube, acho que dá tempo de explicar como de fato me comunico com o mundo externo, fora do meu prédio. Eu falei que meu PC tem o endereço IP interno de 192.168.1.200 e quero assistir um video no youtube. Eu magicamente falei que ele pode se conectar talvez no prédio vizinho cujo endereço é 142.250.219.14.</p>
<p>Deixa eu explicar como essa mágica funciona e ela começa na configuração do seu servidor de DHCP no roteador, onde configurei esse tal de endereço DNS. Isso é um servidor externo e normalmente vem o endereço do servidor de DNS do seu provedor, como da Vivo. Eu recomendo como primeira coisa mudar isso ou pro servidor do Google que é 8.8.8.8 ou da CloudFlare que é 1.1.1.1.</p>
<p>Explicando de forma mais simples, um servidor DNS tem como única função traduzir um endereço em forma de texto como <a href="https://www.youtube.com"target="_blank" rel="noopener">www.youtube.com</a> em um endereço IP. A rede não entende nomes humanos, só entende endereços. Quando seu PC pediu um endereço IP pro DHCP ele ganhou também essa configuração de DNS. Quando no seu PC você digitou <a href="https://www.youtube.com"target="_blank" rel="noopener">www.youtube.com</a> no navegador, o PC checa pra qual DNS deve perguntar e vai ser pro endereço que recebeu do seu DHCP. Todos os outros dispositivos da sua casa vão ter essa mesma configuração, seja seu smartphone, seja sua smart TV.</p>
<p>Se eu abrir um terminal de Linux temos o comando <code>dig</code> e eu posso fazer <code>dig www.youtube.com</code> e volta essa tabela aqui. O que interessa pra gente é esse &ldquo;Answer Section&rdquo;. E eu vejo que a primeira linha é o <code>www.youtube.com</code> que eu pedi. E ele aponta pra um registro CNAME ou canonical name. Um registro CNAME diz pro DNS fazer uma nova pesquisa pra esse nome <code>youtube-ui.l.google.com</code>. Daí volta o resto da tabela que diz que esse domínio aponta pra múltiplos registros A com vários endereços. Registro A é um endereço IPv4, registro AAAA seria endereço IPv6 e já falo disso. Mas e agora, o que eu faço com esse tanto de IPs?</p>
<p>Toda vez que você perguntar pro DNS sobe o youtube, ele vai devolver conjuntos de endereços IP diferentes. Essa é uma estratégia do Google. Ele nunca te dá 100% de todos os registros, porque eles tem literalmente centenas de máquinas pro YouTube, em dezenas de data centers espalhados pelo mundo. Ele vai te devolver alguns endereços de máquinas do data center que é geograficamente mais próximo de você. Daí seu PC recebe esse conjunto e faz um round-robin load balancing. Traduzindo, ele faz balanceamento de carga. Ou seja, numa aba ele vai usar o primeiro endereço. Numa segunda aba, ele talvez vai usar o segundo endereço e assim por diante. Assim você vai sempre caindo em servidores diferentes, pra balancear sua carga.</p>
<p>Cada servidor recebe no pacote algumas informações sobre você, como os cookies do seu navegador. Daí ele sabe que você já tinha feito login num outro servidor e te mantém logado no servidor seguinte. Pra você, é como se estivesse sempre no mesmo lugar, sem saber que por baixo seu PC tá te mandando toda hora pra um desses outros servidores da lista. Um DNS é isso, um serviço que contém uma tabelona apontando um nome de servidor pra um ou mais endereços IP.</p>
<p>Lembram quando falei sobre modo promíscuo que alguém pode estar escutando seus pacotes na rede? Então, um provedor sempre está, entre aspas, em modo promíscuo, porque ele recebe os pacotes de todos os clientes assinantes pra poder mandar pra internet. Então ele é obrigado a receber todos os pacotes pra passar pra frente. Claro, se ele for honesto, assim como sua mãe na extensão, deveria só passar pra frente e não escutar a conversa. Mas, seguro morreu de velho e o certo é assumir que não só eles estão escutando, mas registrando e até revendendo esses dados. Você não tem como saber.</p>
<p>Por isso se fala pra nunca acessar sites que não sejam HTTPS, ou seja, cuja conexão é encriptada. Daí mesmo se alguém no meio escutar seus pacotes não tem como saber o que tá sendo transmitido. Porém, DNS normalmente não é criptografado e antes do HTTPS ativar, o navegador precisa converter <a href="https://www.youtube.com"target="_blank" rel="noopener">www.youtube.com</a> pra um dos endereços IP do Google. E essa pergunta pro DNS é feita em forma não encriptada. Claro, o provedor ou qualquer um no meio do caminho pode só ver o endereço IP e deduzir que é endereço do Google. Mas eu já estou dando a informação que é <a href="https://www.youtube.com"target="_blank" rel="noopener">www.youtube.com</a> de mão beijada. E por isso existem novas tecnologias como DNS over HTTPS ou DNS over TLS que, como o nome diz é o equivalente HTTPS pra DNS.</p>
<p>Isso é simples de ativar, basta colocar na configuração do seu router e seu celular o endereço de um servidor de DNS que suporte DNS over HTTPS, no caso o mais famoso é o 1.1.1.1 do CloudFlare. Mesmo assim, o CloudFlare é outra entidade, o Google também. Eu estaria confiando que eles não estão fazendo nada de errado, o que também é impossível de saber. Mas entre confiar na Vivo ou na Claro, hoje em dia nossa escolha é escolher o menor de dois maus. É por isso que eu pessoalmente uso um DNS caseiro, o Pi-Hole. Como ele cacheia a resolução de endereços, mesmo o CloudFlare pra onde ele conecta não sabe exatamente quando eu pedi esses endereços. Mas isso é tema pra outro dia. Só entenda que estamos meio expostos no setor de DNS ainda.</p>
<p>Enfim, eu fiz uma longa tangente só pra voltar pro assunto de NAT. Agora meu PC tem um endereço IP local graças ao DHCP do meu roteador e eu tenho o endereço IP do youtube, graças a algum DNS como do CloudFlare. Pronto, agora eu posso fazer um pacote com origem sendo 192.168.1.200 e destino sendo 142.250.218.14. Mando o pacote pro default gateway do meu roteador TP-Link. Ele por sua vez roteia o pacote pro Modem na rede 192.168.15.1. E o Modem manda esse pacote pra rede da Vivo.</p>
<p>A rede da Vivo vai enviar pra Internet e aqui vem a parte de roteamento na internet que eu não estou a fim de explicar. Mas pelo menos posso mostrar. No Linux costuma ter um comando chamado <code>traceroute</code> e no Windows tem o comando <code>tracert</code>. Eu chamo então <code>tracert 142.250.218.14</code></p>
<p>e ele vai me mostrar o caminho do pacote da minha máquina até a máquina do Google. Olha só, na primeira linha, como falei, vai pro roteador que é 192.168.1.1. Dele vai pro modem 192.168.15.1 que ele chamou de meuvivofibra.</p>
<p>Lá dentro da rede da Vivo ele passa por outras sub-redes como esse da telesp. Pra quem não sabe a Vivo lá por 2002 ou 2003 foi criada como a junção de várias telecoms pelo Brasil. Em São Paulo, a empresa original era a Telesp. Enfim, dessa rede ele passa pra outra chamada vivozap. E aí vai passando por outros hops até no final chegar no destino. Existem vários serviços com tabelas que associam endereços IP com coordenadas geográficas de latitude e longitude. É uma estimativa. Dá pra chegar perto, mas eu posso mudar o endereço IP de uma máquina numa região pra de outra região. Ainda mais do tamanho do Google que eles podem remanejar grandes grupos de endereços de um data center nos Estados Unidos pro Brasil, por exemplo.</p>
<p>Mas segundo o serviço IP2Location ele diz que esse IP 142.250.218.14</p>
<p>que eu tentei acessar está em Mountain View, na Califórnia, sede do Google. Mas se eu usar outro serviço como o IPapi.co, ele parece ser um pouco mais atualizado e me diz que esse endereço na realidade está em São Paulo. E isso faz mais sentido. O DNS do Google me mandou pra um endereço mais próximo geograficamente de mim, pra ter menos hops ao longo da rota. Quanto mais perto, menor a latência, maior a velocidade. Por isso grandes serviços como Google tem servidores espalhados ao redor do mundo pra garantir que a maioria das pessoas tenham a melhor velocidade possível.</p>
<p>Tá ótimo. Mas e agora? Como que o Google sabe pra quem devolver os pacotes de video? Ué, é só devolver pra origem que tá registrado no pacote, não? Só que lembra? Meu pacote tá com origem 192.168.1.200.</p>
<p>Dentro da rede da Vivo não tem só você. Todo cliente da Vivo vai ter IPs como 192.168 alguma coisa. Vira aquela situação que eu falei do vizinho pentelho que copiou o mesmo número do seu apartamento.</p>
<p>O Google não tem como devolver a resposta pra pessoa certa só com isso. E ele nem precisa, porque ele nunca recebeu esse endereço 192.168.1.200.</p>
<p>E finalmente eu volto pro assunto do tal NAT e como não temos endereços IPv4 suficientes pra todos os dispositivos do mundo. Pra visualizar é a mesma coisa que um número de telefone comercial e ramais. Já pensou que dificuldade que ia ser se uma grande empresa tivesse um número de telefone diferente pra cada pessoa que trabalha lá? Uma empresa com 10 mil funcionários precisando ter 10 mil números de telefone diferentes? Eles não tem. Na realidade normalmente tem um número de telefone comercial que liga num PBX. Esse PBX roteia pra mesa da pessoa certa baseada num número de ramal.</p>
<p>É parecido na Vivo, o NAT é o PBX. Ela tem alguns poucos IPv4 de verdade. Mas internamente dá tipo ramais pra cada cliente. É isso que um NAT faz, no caso um CGNAT, carrier grade, porque é um NAT gigante. Faz um teste, vai no Google e pesquise por &ldquo;what is my ip&rdquo;, qual é meu ip. Qualquer primeiro link serve. No meu caso vai aparecer esse aqui que estou escondendo os últimos dígitos. Esse é o endereço IP da Vivo de saída e é esse endereço que ele carimbou por cima do meu pacote antes de enviar pro Google, e é pra esse endereço que o Google vai devolver os pacotes de video.</p>
<p>Daí o CGNAT da Vivo mantém uma tabelona que mapeia que quando voltar resposta do Google, é pra devolver pro modem certo que fez o pedido. E é por isso que você consegue conectar na internet e se comunicar mesmo não tendo um endereço IPv4 válido na internet. O CGNAT faz o trabalho de enviar e devolver os pacotes pras sub-redes corretas. Literalmente Network Address Translator ou tradutor de endereço de redes. O problema disso é que você consegue se conectar nos outros, mas outros de fora não conseguem achar você diretamente. Pra alguém de fora se conectar em você, primeiro você precisa se conectar nele pra abrir uma conexão.</p>
<p>Vou deixar como lição de casa. No mundo de hoje que fazemos chamadas de Zoom, ligação via Whatsapp e outras formas de Voz sobre IP ou VoIP, o requerimento é que duas pontas só conseguem se conectar diretamente se ambos os lados conseguem se encontrar na internet. Mas normalmente você e a pessoa que quer falar estão escondidas numa sub-rede privada atrás de um NAT de provedor. Portanto nenhum dos dois lados está acessível. Como que se cria uma conexão ponto a ponto, sem precisar de intermediários? Pra isso existem formas como ICE que é STUN/TURN e protocolos como RTP. Dêem uns googles aí de como isso funciona. Não vou tentar explicar hoje.</p>
<p>Voltando ao problema de endereços IP serem 32-bits e permitirem no máximo 4 bilhões de endereços. A essa altura já era pra ter acabado esses endereços. Se uma Vivo tiver, sei lá, 100 milhões de clientes, ia precisar de 100 milhões de endereços IPv4, pelo menos, sem contar que cada cliente tem hoje um PC, uma smart TV, alguns smartphones, então fácil ia precisar de meio bilhão de endereços. Mas graças ao CGNAT, faz de conta, com 1 único endereço IPv4 ela pode manter esse meio bilhão de dispositivos em redes privadas escondidas com endereços 192.168 ou 10 ponto da vida por trás. E é assim que a gente evitou acabar os IPv4. Mas isso é um paliativo.</p>
<p>NAT ajudou a evitar o colapso da internet por falta de endereços mas ao mesmo tempo dificultou e complicou muita coisa, como o que falei de Voz sobre IP e todo tipo de conexão ponto a ponto, como você conseguir jogar online com seus amigos. Como na prática todo mundo tá escondido em redes privadas atrás dos provedores, estamos à mercê da boa vontade deles pra muita coisa. Esse problema não é novo, já se falava do problema de acabar endereços desde o fim dos anos 80. Desde o começo dos anos 90 já se começou pesquisas sobre como resolver esse problema. E já temos a solução na forma do IPv6 desde 1998.</p>
<p>Você já deve ter visto IPv6. É um número de 128 bits. Eu venho dizendo que 64 bits é um número absurdo. Computadores modernos de 64 bits não tem barramentos de 64 bits ainda, o máximo acho que é 48 bits, que já é endereço de memória como se não houvesse amanhã mesmo pra um supercomputador. Pra ter uma idéia, a maioria das pessoas hoje já acha 128 gigabytes de RAM algo absurdo. Pra endereçar 128 giga um barramento de 37 bits seria suficiente.</p>
<p>2 elevado a 128 é um número tão absurdo que um ser humano normal não consegue imaginar. Imagina a superfície inteira do planeta Terra, incluindo a superfície dos oceanos, inteiramente coberta por PCs. Eu poderia ter 7 x 10 elevado a 23 endereços IP por metro quadrado. Conseguem entender? 10 seguido de 23 zeros de quantidade de PCs, por metro quadrado. Estudantes de química vão notar que esse número é maior que o número de Avogadro. Literalmente seria possível dar um endereço IP pra cada molécula da superfície da Terra e sobrar. É astronomicamente grande.</p>
<p>Na prática isso significa que todo dispositivo em existência hoje, todo PC, todo servidor, cada smartphone, cada dispositivo inteligente da sua casa como sua TV ou sua geladeira, cada uma poderia ter um endereço IP único com facilidade, por gerações a fio. E fazendo isso não precisaríamos mais estar atrás de um CGNAT. Todo mundo poderia estar diretamente na internet sem intermediários, o que facilitaria muito todo tipo de serviço ponto a ponto como voz sobre IP, e nunca teríamos que nos preocupar de novo com acabar endereços e ter que fazer gambiarras como NAT.</p>
<p>O problema é que o sucesso da internet evitou que pudéssemos migrar pra melhor solução a tempo. IPv6 surgiu no calor da febre da bolha da Internet do fim dos anos 90. Pra maioria da população, nem se sabia se essa tal de internet ia mesmo dar certo. Muita gente achava que era só um troço passageiro que ia morrer logo. Coisa de hipster. Usar internet no fim dos anos 90 era opcional, e muita gente não tava interessada. Pra dar certo, investiu-se tudo no que já existia e se sabia que funcionava, que era IPv4. Por que a gente ia se preocupar em acabar endereços se ninguém nem sabia se ia entrar gente suficiente pra acabar os endereços? Migrar naquele momento pra IPv6 parecia uma otimização prematura.</p>
<p>E pra piorar, tivemos o crash da bolha da internet em 2001. Aí sim, ninguém ia investir milhões de dólares pra migrar toda a infraestrutura de tudo pra IPv6. Pra tudo funcionar perfeito, o certo, seria o mundo inteiro migrar quase que tudo de uma vez pra IPv6. Senão como alguém com IPv4 vai conectar num servidor IPv6? Pra isso foram sendo inventadas formas de migração menos agressivas como NAT de IPv6 pra IPv4. Temos NAT64 por exemplo, que permite que PCs que já usem endereço IPv6 consigam conectar em servidores que ainda são IPv4. Mas a moral da história foi essa: o inimigo do IPv6 foi o sucesso do IPv4.</p>
<p>E vocês sabem, se alguma é opcional, a maioria sempre vai esperar até a última hora. Por que eu vou gastar meu tempo pra ter todo esse trabalho se todo mundo ainda vai estar usando o esquema antigo? Vou ficar no antigo também. É a lei do menor esforço. Migração pra IPv6 só vai acontecer o dia que for mandatório, tipo os governos decidirem que ninguém mais acessa nenhum serviço do governo se não for IPv6. Mas governos normalmente são os últimos a adotar qualquer coisa nova. Eu não esperaria muito deles. Enquanto isso a gente vive num mundo em eterna transição entre IPv4 e IPv6. Alguns provedores já suportam nativamente, alguns você pode ligar e pedir pra habilitar roteamento por IPv6, mas não tem um padrão. Cada caso é um caso.</p>
<p>Tem uma questão de privacidade também. Se todo dispositivo tiver um IPv6 estático, seria como um MAC address que nunca muda. Hoje em dia ninguém do lado de fora pode dizer com 100% de certeza quem é você porque só enxerga o IP público do provedor e não seu IP privado da rede atrás do NAT. Mas se for direto com IPv6 dá pra dizer com certeza que veio do seu dispositivo. Por isso sistemas operacionais como Windows usam números IPv6 temporários pra se comunicar e garantir algum nível de privacidade. Não recomendo desligar isso. Mas veja, se eu der o comando <code>ipconfig</code> no meu Windows eu vejo que eu tenho um IPv6 estático na minha placa de rede, que obviamente estou obfuscando uma parte, e embaixo tenho o IPv6 temporário, que é o que um servidor iria ver se eu conectar direto nele sem NAT.</p>
<p>De qualquer forma, se IPv4 até dava pra você decorar. IPv6 é mais difícil. Primeiro o número é longo, então a notação pra ficar menos difícil digitar foi separar oito blocos de 16 bits por dois pontos. Mas diferente de IPv4 onde escrevemos cada bloco no formato decimal mesmo, de 0 até 255, 16 bits é um número que vai até mais de 65 mil então ficaria longo. Por isso escrevemos em hexadecimal, onde cada dígito vai de 0 até F, assim o bloco precisa de no máximo 4 dígitos hexadecimais. São 8 blocos de 4 dígitos hexadecimais separados por dois pontos.</p>
<p>Se escolher direito, pelo menos no começo, vai ter muito endereço com vários blocos de zero no meio. Pra facilitar, o protocolo permite omitir esses blocos. E finalmente, endereços IPv4 podem ser escritos como IPv6 colocando dois dois pontos antes do endereço escrito no mesmo formato de antes. Enfim, pra maioria tudo isso é automático, porque o modem do provedor vem com servidor de DHCP e ponto de acesso wifi, então ele automaticamente te dá endereço IP, configura seu DNS, e basta você se conectar no Wifi que já tá navegando. Mas programadores precisam saber que esses detalhes existem. Em breve muitos de vocês vão precisar lidar com esse problemas, como conectar pessoas num app de comunicação ponto a ponto tendo que furar NAT ou implementar IPv6.</p>
<p>E com isso terminamos essa parte da mini-série sobre o básico de redes. Isso resume, super resumido, os capítulos de camada física, camada de data link, camada de rede e um pouco da camada de transporte do modelo OSI, com um pezinho na camada de aplicação que é a parte mais prática de verdade e que devo tentar falar mais em outros episódios. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e compartilhem o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>redes</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #121 - Introdução a Redes: Como Dados viram Ondas? | Parte 1</title><link>https://akitaonrails.github.io/2022/07/01/akitando-121-entendendo-transferencia-de-sinais-digitais-introducao-a-redes-parte-1/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/07/01/akitando-121-entendendo-transferencia-de-sinais-digitais-introducao-a-redes-parte-1/</guid><pubDate>Fri, 01 Jul 2022 18:09:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/0TndL-Nh6Ok"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Finalmente vou começar a explicar o básico do básico de redes pra programadores iniciantes finalmente entender como de fato a Internet funciona, pra no futuro poder explicar melhor como aplicações web são arquitetados e como coisas como Docker conseguem funcionar.&lt;/p&gt;
&lt;p&gt;Esta é a Parte 1 de uma mini-série inicial de 3 episódios. Assinem o canal pra não perder o resto!&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/0TndL-Nh6Ok"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Finalmente vou começar a explicar o básico do básico de redes pra programadores iniciantes finalmente entender como de fato a Internet funciona, pra no futuro poder explicar melhor como aplicações web são arquitetados e como coisas como Docker conseguem funcionar.</p>
<p>Esta é a Parte 1 de uma mini-série inicial de 3 episódios. Assinem o canal pra não perder o resto!</p>
<h2>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>00:00 - Intro</li>
<li>00:53 - Modem da Vivo</li>
<li>01:52 - Bits vs Bytes</li>
<li>02:59 - Primórdios da Internet discada</li>
<li>05:10 - Fibra ótica e cabo de cobre</li>
<li>08:04 - Wifi</li>
<li>10:11 - 5G</li>
<li>14:31 - Preciso de quanta velocidade?</li>
<li>16:24 - Primórdios da comunicação: telefone de copinho</li>
<li>19:19 - Telecom e computadores</li>
<li>22:27 - teclados de novo??</li>
<li>23:18 - Par trançado, multiplexing e modulação</li>
<li>27:10 - Modem discado e ADSL</li>
<li>31:23 - Datagramas</li>
<li>33:08 - De comutação de circuito a pacote</li>
<li>36:30 - Conclusão</li>
</ul>
<h2>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li>Cheat sheet: which 4G LTE bands do AT&amp;T, Verizon, T-Mobile and Sprint use in the USA? (<a href="https://www.phonearena.com/news/Cheat-sheet-which-4G-LTE-bands-do-AT-T-Verizon-T-Mobile-and-Sprint-use-in-the-USA_id77933"target="_blank" rel="noopener">https://www.phonearena.com/news/Cheat-sheet-which-4G-LTE-bands-do-AT-T-Verizon-T-Mobile-and-Sprint-use-in-the-USA_id77933</a>)</li>
<li>What are the differences between 2G, 3G, 4G LTE, and 5G networks? (<a href="https://rantcell.com/comparison-of-2g-3g-4g-5g.html"target="_blank" rel="noopener">https://rantcell.com/comparison-of-2g-3g-4g-5g.html</a>)</li>
<li>How DSL Works (<a href="https://computer.howstuffworks.com/dsl.htm"target="_blank" rel="noopener">https://computer.howstuffworks.com/dsl.htm</a>)</li>
<li>Why are Wires Twisted in Twisted Pair Cables? (<a href="https://www.systoncable.com/why-are-wires-twisted-in-twisted-pair-cables/"target="_blank" rel="noopener">https://www.systoncable.com/why-are-wires-twisted-in-twisted-pair-cables/</a>)</li>
<li>Ethernet cable speed categories explained (<a href="https://www.allconnect.com/blog/what-ethernet-cord-do-you-need"target="_blank" rel="noopener">https://www.allconnect.com/blog/what-ethernet-cord-do-you-need</a>)</li>
<li>Coaxial vs Ethernet cable - What&rsquo;s the Difference? (<a href="https://www.truecable.com/blogs/cable-academy/coaxial-vs-ethernet-cable-whats-the-difference"target="_blank" rel="noopener">https://www.truecable.com/blogs/cable-academy/coaxial-vs-ethernet-cable-whats-the-difference</a>)</li>
<li>[Akitando #79] Teclados Mecânicos Exóticos | Minha coleção - Parte 2 (<a href="https://www.akitaonrails.com/2020/05/12/akitando-79-teclados-mecanicos-exoticos-minha-colecao-parte-2"target="_blank" rel="noopener">https://www.akitaonrails.com/2020/05/12/akitando-79-teclados-mecanicos-exoticos-minha-colecao-parte-2</a>)</li>
<li>V.90 Dial-up Modem Handshake - Transactional Analysis (<a href="https://www.youtube.com/watch?v=VaWpi9o_hHI"target="_blank" rel="noopener">https://www.youtube.com/watch?v=VaWpi9o_hHI</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Quem me acompanha no instagram viu minha saga atualizando meu armazenamento pra um NAS maior e mais rápido, inclusive os problemas que tive e como resolvi. Mas durante as semanas que postei stories, recebi dezenas de mensagens diretas com dúvidas elementares. Mesmo programadores tem buracos de conhecimentos importantes quando se fala de redes.</p>
<p>Eu não sou nenhum especialista em redes, bem longe disso, e é um dos motivos de porque evitei falar do assunto por tanto tempo. Minha intenção não é falar sobre tudo que existe do assunto, precisaria de um canal inteiro pra isso. O que quero falar neste e nos próximos dois episódios são só alguns conceitos que podem ser úteis pra iniciantes. Sem entender nem o que vou explicar nesta mini-série, não tem como entender coisas como Docker, que eu quero falar no futuro.</p>
<p>(&hellip;)</p>
<p>Vamos começar com coisas que todo mundo tem em casa. Esse aqui é meu modem da Vivo, estou com um plano de fibra ótica de 300 megabits por segundo. Olha aqui, esse fininho é a fibra ótica. Vamos checar? Só digitar &ldquo;speedtest&rdquo; no Google e você pode usar esse aqui do próprio Google, ou esse aqui embaixo da Ookla que é bem famoso, ou o Fast do Netflix, que por alguma razão dá números um pouco mais inflados. Cada um vai dar medidas um pouco diferentes mas no geral é próximo da velocidade de verdade. Vamos ver os três rodando um pouco.</p>
<p>Olhem só como é diferente, mas uma coisa é certa, o download tá ok dentro do plano de 300 megabits por segundo. Nos três fica até um pouco acima disso, mas o upload é sempre lá pela metade dessa velocidade. Provedores sempre fazem isso porque você nota mais lentidão em download do que upload. Mas tudo bem, um episódio aqui do canal fica na faixa de 10 a 15 gigabytes. Nessa velocidade eu consigo subir pro YouTube em aproximadamente 15 minutos. Não é ruim. Agora, você consegue saber como chega nessa estimativa?</p>
<p>Muita gente vê os planos escritos 100 mega, 300 mega, e assume que é 100 megabytes ou 300 megabytes, mas não, são megabits. E isso faz muita diferença. Lembram? 1 byte são 8 bits. Portanto 300 megabits você divide por 8 pra ter em megabytes. 300 dividido por 8 é 37 megabytes por segundo, esse é o máximo que vai conseguir em downloads. Se for um plano de 100 megabits, divide por 8, não é muito mais que 12 megabytes por segundo de download. Essa é a velocidade que interessa.</p>
<p>Quando se fala em velocidades de rede, por alguma razão se fala em bits em vez de bytes. Eu acho que tem o lance de pro marketing parecer melhor pra colocar números grandes em propagandas, mas acho que tem a ver com o histórico de que antigamente, estou falando de década de 60, transmitir 300 bits por segundo era considerado bastante. Eu mesmo cheguei a usar modem de linha discada de 9600 bits por segundo. Em 1991, a velocidade média era de 14 kilobits por segundo. Se dividir por 8 pra ter em bytes, é nada menos que 1.75 kilobytes por segundo. Pouco mais de mil e setecentas letras de 1 byte cada, por segundo.</p>
<p>No fim dos anos 90, começo dos anos 2000, a maioria das pessoas estava em modems de 56 kilobits por segundo. Só quando começamos a ter ADSL com serviços como o Speedy, que chegamos perto do elusivo 1 megabit por segundo, que ainda assim, dividido por 8 são só 128 kilobytes por segundo. Lembro quando eu vi um demo da Ajato lá por 1993, e download de 1 megabit parecia a coisa mais rápida do mundo.</p>
<p>Deixou o eu no colegial daquela época realmente imaginando que chegamos no futuro. Agora pensa, um arquivo de música mp3 de qualidade média é alguma coisa na faixa de 5 a 10 megabytes. Digamos que seja 10 megabytes. Quanto tempo leva pra baixar esse arquivo com um modem do fim dos anos 90 de 56 kilobits por segundo?</p>
<p>A conta é 10 megabytes vezes 1024 pra ter em kilobytes. 1024 porque estamos falando em binário e isso é 2 elevado a 10. Tem toda uma controvérsia de usar múltiplos de 1000, mas isso é só marketing. O certo é sempre potências de 2. Daí divide isso por 56 kilobits, dividido por 8 pra ser bytes. E isso vai dar uns 1462 segundos, dividido por 60 e vai dar nada menos que 24 minutos.</p>
<p>Quase meia hora pra baixar uma música que não tem muito mais que 5 minutos de duração. Essa é a era antes do streaming ser possível. Lembram do episódio que expliquei compressão? Pra streaming de áudio ser possível, eu precisaria no mínimo ser capaz de baixar essa música nos mesmos 5 minutos de duração. Por isso que não existia streaming nos anos 90 e começo dos anos 2000.</p>
<p>A velocidade mínima seria esses 10 megabytes do mp3 divido por 5 minutos, ou seja, 2 megabytes por minuto. Isso vai dar 40 kilobytes por segundo, ou 320 kilobits por segundo. Nem é muita coisa, mas o modem da época de 56 kilobits era 6 vezes mais devagar. Os anos 2000 não foram fáceis não, por isso a gente deixava um monte de downloads enfileirados a noite inteira pra conseguir baixar um álbum de música. Em pouco mais de 20 anos fomos de 1 megabit pros 300 megabits que tenho em casa agora. 300 vezes mais rápido, e isso nem é o máximo que dá pra chegar. Nos Estados Unidos, Canadá, Japão e outros lugares existem alguns planos de 1 gigabit por segundo.</p>
<p>Quando falamos em velocidades assim, pensamos em fibra ótica, que é o que tem nesse meu modem. Pense numa fibra ótica como um tubinho muito fino de fibra de vidro por onde passa luz. Literalmente se eu acender luz numa ponta, vai acender na outra ponta. Vocês já devem ter visto em algumas baladas.</p>
<p>Se não me engano um bom cabo de fibra ótica, e tem tipos diferentes, tem capacidade de transmitir até 10 gigabits por segundo por uma faixa de 5 a 10 quilômetros. Isso é máximo teórico. Distância afeta banda, porque mesmo numa fibra ótica existe perda de sinal. Em todo cabo, quanto maior a distância, menor a velocidade porque maior vai ser a perda de sinal e interferência no meio do caminho.</p>
<p>Abaixo de fibra ótica estão cabos de rede como este, cabos ethernet, com conector RJ-45. Eles são divididos em categorias. Na prática à medida que conseguimos ir melhorando os processos de fabricação e os processadores de switches e roteadores, fomos subindo de categoria. E a gente classifica esses cabos pela categoria ou cat. Esse na minha mão acho que é o mais comum, Cat 5e. Na dúvida hoje em dia, assuma que deve ser cat 5e ou até cat 6.</p>
<p>Cat 1 até 3 acho não se usa mais, não lembro se teve cat 4. Era pra redes inferiores a 10Base-T, ou seja, menos que 10 megabits por segundo pra baixo. Lembra que nos anos 2000 a gente male male tava falando de 1 megabit pra internet? 10 megabits era 10 vezes isso, tava ótimo pra época. Um CD-ROM ocupava 650 megabytes, seria menos de 10 minutos pra transferir numa rede de 10 megabits. No cat 5 subimos pra rede de 100 megabits. Alguns lugares com cabeamento antigo ainda devem ser cat 5. Mas tudo isso já é considerado obsoleto.</p>
<p>Com cat 5e fomos pra rede de 1000 megabits ou 1 gigabit por segundo. Eu acho que a maioria dos computadores, pelo menos dos últimos 10 anos pra cá, se tem placa de rede, é 1 gigabit. A maioria das placas mãe de boas marcas como Gigabyte ou MSI já tem com porta de ethernet 1 gigabit. A minha placa mãe mais moderna que é o X570s vem com porta de 2.5 gigabits. Cabos mais modernos são categoria 6 e 7, que tem especificações de construção mais exigentes e conseguem suportar sinais na faixa dos 500 Mhz e conseguem ir até os 10 gigabits por segundo de redes modernas. Mas até pra redes 1 gigabit vão ser melhores que cat 5e. Na dúvida, só compre cabos cat 6. Cat 7 e cat 8 a maioria não vai precisar.</p>
<p>1 gigabit por segundo numa rede local tá ótimo. A internet que a maioria das pessoas tem em casa é 10 vezes mais lento que isso. Então dá e sobra pra conectar múltiplos dispositivos na rede, compartilhar a internet e ainda sobra banda pra transferir arquivos localmente e tudo mais. Pra fazer streaming de video, na prática, acima de 3 megabits já é possível. Ou seja, a maioria das pessoas não lida com nada muito maior que 10 megabits. Rede 1 gigabit é 100 vezes isso.</p>
<p>Considere também as redes sem fio como do seu celular. Se estiver numa boa região metropolitana com 4G LTE pelo menos, as velocidades são mais ou menos. No meu celular eu consigo a faixa de 60 a 70 megabits por segundo pra download e menos de 5 megabits pra upload. Eu pessoalmente acho horrível, mas pra média da população isso é rápido o suficiente. De novo, acima de 3 megabits dá pra fazer streaming, receber mensagem de áudio e tudo mais, então tá ótimo.</p>
<p>Falando nisso, saiu faz pouco tempo o novo padrão Wifi 6 que promete ser umas 3x mais rápido que Wifi 5, que é o que todo mundo tem. Mas Wifi 5 não é grande coisa também. Vamos fazer outro teste rápido. Esse é o Librespeed, uma versão open source do Speedtest do Google que mostrei lá no começo. É um teste de download e upload do meu PC pro meu NAS usando rede cabeada. Tá ótimo, faixa de 6 gigabits de download e de upload.</p>
<p>Minha rede é de 10 gigabits, mas depois falo disso. Agora vamos ver meu smartphone conectado via wifi na mesma rede acessando o mesmo serviço. É essa coisa ridícula aqui de faixa de 200 megabits por segundo. Wifi é umas 3x mais lento que uma rede 1 gigabit que todo mundo tem. Mas é mais rápido que a internet do seu provedor então, de novo, tá ótimo.</p>
<p>Wifi e outras formas de comunicação sem fio usam o ar como o meio de transporte das ondas em vez de cabos de cobre. Mesmo com paredes no meio do caminho, a onda consegue passar e continuar em frente, embora perdendo força. Você sabe disso porque se gritar de um quarto, sua mãe vai te xingar lá da cozinha. A onda da sua voz atravessa paredes e chega até lá. Tudo que oferece resistência vai diminuir o sinal e introduzir barulho. Por isso que você não consegue ir tão longe do seu roteador sem notar que a velocidade caiu bastante.</p>
<p>Sua voz tem baixa frequência, por isso atravessa paredes fácil. Quanto maior a frequência, mais difícil é atravessar paredes, por isso que o Wifi mais comum é o que opera na faixa de 2.4Ghz e você consegue ir até uns 100 metros de distância e ainda navega, mesmo que mais lento. Mas na rede mais rápida de 5Ghz, uma ou duas paredes e você já nota que fica bem ruim de usar.</p>
<p>Isso é outro assunto que daria uma tangente enorme, mas basta saber que essas bandas de frequência são convenções definidas por política. Várias organizações estabeleceram quais bandas podem ser usadas pra quais coisas. A faixa de 900 Mhz e 2.4 Ghz, por exemplo, são chamadas de ISM ou Industrial, Scientific, Medical, que são bandas não licenciadas que podem ser livremente usadas sem interferir com aplicações militares, por exemplo. Mas por isso também os canais dessas bandas costumam ser super congestionadas porque coisas como seu telefone sem fio, controle remoto de garagem e tudo mais operam nas mesmas bandas que seu Wifi e tudo isso causa interferências.</p>
<p>Por causa disso, a partir de 2001 a organização FCC abriu a faixa de 5Ghz até 64Ghz pro público. É nela que trafega coisas como TV aberta em alta definição, que sua TV capta por antena. É na parte mais baixa dessa banda, nos 5Ghz que opera a maioria dos pontos de acesso de Wifi. Na prática, quanto maior a frequência, maior a velocidade, mas menos área o sinal cobre e mais dificuldade tem de passar obstáculos, como paredes. É o problema do 5G, que tem várias versões. Se você não sabia, 5G não é só uma coisa, são diferentes bandas de frequência.</p>
<p>No começo dos anos 2000 a gente experimentou redes sem fio 2G, que ainda era comutação de circuito e velocidades de pico na faixa de 64 kilobytes por segundo. E cuidado que wireless a gente fala em velocidade de pico, que é em situação ideal, com sinal limpo, pouca ou nenhuma interferência. Na vida real a velocidade vai ser menor e variar bastante dependendo de onde você está. Mas em seguida tivemos 3G, na época do iPhone 3G lá por 2008. Agora já era packet switch indo até uns 2 megabits por segundo, e uma revisão que veio depois, o 3G HSPA+ que podia ir de uns 8 megabits até 21 megabits por segundos, usando a banda de de 15 a 20 megahertz.</p>
<p>Eu tô simplificando bastante porque 3G, 4G, 5G são famílias de tecnologias. Cada uma usa diversas bandas diferentes de frequências. Por isso o 4G de um provedor pode ser diferente do 4G de outro provedor. Por exemplo, nos Estados Unidos a T-Mobile usava bandas de 800 Mhz até 1900 megahertz. Mas a Sprint usava banda de 850 até 2500 megahertz. Velocidade máxima teórica de 4G seria equivalente a uma rede de 1 gigabit, mas na prática não ia muito mais que 50 megabits por segundo.</p>
<p>A gente sempre operou wireless em faixas abaixo de 2 gigahertz. A tal quinta geração ou 5G entra numa categoria de ondas chamado Extreme High Frequency, literalmente frequências extremamente altas, que vai de 30 até 300 gigahertz. Isso é o que se chama de millimiter wave. Pra comparação, o que a gente chama de microondas ou microwaves é de 3 a 30 gigahertz. Millimiter wave começa nos 30 gigahertz. Eu posso estar falando bobagem, mas acho que o que se chama de 5G comercial hoje em dia é bem abaixo disso, um pouco mais que a faixa de 4G LTE. É mais rápido, mas não tanto quanto se espera.</p>
<p>Millimiter wave é muito mais rápido, subindo do máximo teórico de 4G de 1 gigabit e indo direto até 10 gigabits. De novo, teórico, porque apesar da alta velocidade, a altíssima frequência significa que tem alcance muito menor, muito mais direcional, e é muito fácil de interferir, por isso tem dificuldade de atravessar obstáculos como prédios entre o celular e as antenas. Logo, precisa de muito mais antenas pra cobertura e por isso demora tanto pra ter em todo lugar. Não espere ver essa banda perto de você tão cedo.</p>
<p>Na prática é só entender que transmitimos dados como ondas usando o ar como meio. Existem várias faixas de frequências úteis que vão de 3 kilohertz até o topo de 300 gigahertz. Politicamente várias dessas faixas foram reservadas pra usos específicos, por exemplo, rádio AM dentro da banda que vai de 300 kilohertz até 3 megahertz, rádio FM dentro da banda que vai de 30 megahertz até 300 megahertz, 4G que tá banda seguinte até 3 gigahertz.</p>
<p>E agora explorando millimiter wave que começa em 30 gigahertz e antes era usado mais pra comunicação via satélite, astronomia e coisas assim. Tudo isso porque aprendemos a modular e demodular sinal digital em analógico, e graças a pesquisadores como Claude Shannon sabemos como lidar com interferência e garantir que os pacotes que recebemos estão intactos.</p>
<p>Estou dando todos esses números pra vocês terem um pouco de ordem de grandeza das coisas. No mundo de data centers, os engenheiros estão lidando com redes acima de 10 gigabits, Fibra ótica e tudo mais. No mundo caseiro, a média da população ainda tá na era da rede 100 megabits, tecnologia de uns 20 anos atrás. Mas como eu disse, a média da população não lida com petabytes de dados. A maioria dos HDs de vocês male male tem meio terabyte, um quarto de terabyte. O maior volume de transferência é streaming de video. Realmente não precisa de mais que 100 megabits. 1 gigabit já tem muita sobra.</p>
<p>Eu acho 300 mbps de download e 150 mbps de upload da Vivo muito lento. Mas por outro lado, eu também não uso mais que isso no dia a dia, só de vez em quando queria ter internet gigabit. Mas o custo não compensa, por isso aguento com velocidades baixas assim. Um exemplo de eventos raros onde isso faz diferença. Estou fazendo backup do meu backup no serviço AWS Glacier. É tipo um S3 mas feito pra backups e coisas que não se acessa o tempo todo, então é mais barato pra armazenar. Estou transferindo uns 15 terabytes de dados pra lá. Quanto tempo acha que leva, nesse plano da Vivo de meros 150 megabits de upload?</p>
<p>Vamos fazer as contas. 150 megabits, dividido por 8, é menos de 19 megabytes por segundo. 15 terabytes, multiplicado por 1024 dá 15.360 gigabytes, multiplicado de novo por 1024, dá mais de 15 milhões de megabytes. Agora dividindo pela velocidade de 19 megabytes por segundo, vai dar mais de 800 mil segundos.</p>
<p>Dividido por 3600 dá mais de 229 horas, dividido por 24 horas, dá pouco mais de 9 dias. Isso sem contar horas do dia que a velocidade fica instável ou cai. Fácil isso vai dar mais de 10 dias pra eu conseguir transferir tudo pra lá. Por isso eu disse que é uma velocidade horrendamente baixa. Só que só retardados como eu faz uploads de 15 terabytes de coisas. Não é o normal.</p>
<p>Já que estamos falando de modem, eu queria fazer uma tangente pra dar outra noção que muitos não devem ter. Como que os dados são transportados na fibra ou em cabos como coaxial ou ethernet? Pra explicar isso precisaria de mais física do que estou confortável pra explicar então vou usar um exemplo beeeem simplificado só pra ter uma imagem na cabeça. Lembra quando vocês eram criança e brincavam de telefone de copinho? Como que fazia? Prendia cada ponta de um fio bem longo no fundo de dois copos e ia longe até esticar o cabo sem arrebentar.</p>
<p>Quando você gera voz, tá empurrando o ar com uma certa frequência e amplitude pra cada fonema. Já viram como a voz é representada num programa de edição como esse aqui? Tudo que eu vou falando é representado como ondas. Olha só, aaaaa &hellip;. bbbbb &hellip;. ccccc &hellip;. No mundo físico isso são minhas cordas vocais vibrando e empurrando o ar. Isso vai bater no fundo do copo e vibrar o fio conectado nele.</p>
<p>Se o fio estiver tensionado o suficiente, ele vai transferir essas ondas até a outra ponta. E o copo no final vai servir pra amplificar um pouquinho o que recebeu e empurrar o ar até seus ouvidos. Na real &ldquo;empurrar&rdquo; não é o  termo adequado porque não está movendo nada, é como numa bacia de água em repouso quando vc toca numa ponta, gera uma onda, que vai até outra ponta, mas se tiver algo boiando no meio, ela continua no mesmo lugar. A água, o ar, ou o fio, servem de meio por onde as ondas vão trafegando. E a voz e outros sons são formados pelas diferenças em amplitude e frequência dessas ondas.</p>
<p>É mais ou menos assim mesmo que os telefones antigos também funcionavam, com materiais um pouco menos rudimentares que copinhos de plástico e barbante. Mas nesse modelo, só dá pra ligar uma pessoa até outra pessoa. E se eu quisesse falar com pessoas diferentes? Já viram filmes antigos que tinha telefonistas? Você estava sempre conectado com uma central onde atendia uma telefonista. Daí dizia pra ela, &ldquo;me conecta com fulano de tal&rdquo; e aí ela pegava um fio e ligava num painel. Era ela ligando o fio do seu telefone, via esse painel, com o fio do outro telefone, fechando um circuito.</p>
<p>Isso é no começo da telefonia comercial, um século atrás quase, claro, depois eliminou-se essa posição de pessoa ligando cabos e passou a se usar sistemas que conectavam números de telefone automaticamente, os switches. Nem eu antigamente nunca falei com uma telefonista, era só discar o número e esperar a outra ponta atender. Mas imagine que é o mesmo cenário, onde se faz switching, ou comutação, e fecha um circuito entre as duas pontas, como no telefone de copinhos.</p>
<p>Isso se chama circuit switching ou comutação de circuito. E nesse caso a linha entre eu e a outra pessoa fica dedicada a essa sessão até uma das pontas desconectar. Por muitos anos foi assim que funcionou, toda casa tinha uma linha telefônica ligada numa central, e a gente pagava por minuto que a sessão ficava aberta entre dois telefones. O sistema automático que substituiu a telefonista fazia o switching e a bilhetagem. E foi assim que o mundo se conectava.</p>
<p>Em paralelo, estamos falando dos anos 60 a 70, no mundo empresarial estavam experimentando com esse tal de computador, que como todo mundo já viu em fotos antigas, eram equipamentos gigantes que ocupavam um quarto inteiro. Super quentes, e pros padrões de hoje, extremamente fracos. Se falarmos de começo dos anos 70, um PDP-11 tinha CPU de 16-bits e poderosos 15 megahertz e na faixa de pouco mais de 2 kilobytes de RAM, acho que podia ir até 32 kilobytes.</p>
<p>Se você não consegue entender essa ordem de grandeza pense no seu celular onde está assistindo esse video, ou seu notebook. Ele tem uma CPU de múltiplos cores onde cada um roda na faixa de 2 gigaherz ou mais. Giga é mil vezes mais que mega. Estamos na ordem de processamento de mais de mil vezes um processador dos anos 70. E você tem mais de 1, talvez uns 8, rodando em paralelo. No PDP-11 era um só de 15 megahertz.</p>
<p>Memória então, nem se fala. Seu celular talvez tenha o quê, uns 4 gigabytes? Gigabyte é 1024 vezes megabyte que é 1024 vezes kilobyte. Você está numa ordem de grandeza de um milhão de vezes mais memória que se tinha nos anos 70. Não é dobro, não é 100 vezes, é um milhão de vezes.</p>
<p>Até aproveitando, muita gente me pergunta qual computador é ideal pra aprender a programar e a resposta é óbvia: qualquer um que você conseguir comprar. Não existe &ldquo;computador melhor pra aprender&rdquo;. O computador mais furreca que você comprar hoje é mil vezes melhor do que eu tinha pra aprender nos anos 80. Se eu aprendi a programador num PC na faixa de 6 megahertz com 1 mega de RAM, não vejo porque você não possa aprender a programar num Raspberry Pi de 1 gigahert com 1 gigabyte de RAM. Quem realmente quer aprender, aprende com qualquer equipamento que tiver acesso e aprende a espremer o máximo dele. Se você não consegue extrair o máximo de um computador fraco, um computador mais forte é um desperdício pra você.</p>
<p>Só que nos anos 70, 15 megahertz de processamento e 2 kilobytes de RAM era bastante coisa e super caro. Coisa da faixa de 150 mil dólares, em dólares dos anos 70. Ajustado pela inflação seria mais de 1 milhão de dólares de 2022. É um puta investimento. E custando caro assim, obviamente eram recursos que precisavam ser divididos por mais pesquisadores e engenheiros ao mesmo tempo. Desses esforços que surge coisas como o sistema operacional UNIX cuja função era não só facilitar escrever aplicativos, mas compartilhar a máquina com mais de um usuário. É o conceito de time sharing, ou compartilhamento de tempo.</p>
<p>Pra conseguir ter várias pessoas usando o mesmo computador ao mesmo tempo, primeiro o sistema operacional precisava dividir o tempo de processamento e a memória pra vários aplicativos rodando em paralelo. Eu fiz dois vídeos explicando como se faz isso, mas em resumo ele deixa um aplicativo rodar por um tempo, daí pausa ele, e dá tempo pra outro aplicativo de outra pessoa rodar um pouco. Daí pausa esse, e resume o anterior que tava pausado. No total cada um vai levar mais tempo pra rodar, mas pelo menos os dois puderam iniciar os trabalhos ao mesmo tempo e não ter que ficar esperando na fila de braços cruzados esperando o outro acabar.</p>
<p>Quando esse tipo de sistema começou a ficar possível, agora seria mais conveniente que cada pessoa tivesse seu próprio monitor e teclado, pra realmente trabalhar simultaneamente no mesmo computador. E assim começou a surgir os tais terminais burros. Na parte 2 dos meus videos de teclado eu explico um pouco como isso funcionava. E parte das tecnologias do mundo de telecomunicações passaram a entrar nos computadores.</p>
<p>Como um exemplo pequeno, eu tenho aqui esse teclado IBM Model M dos anos 80 e ele não tem um cabo USB, óbvio, mas também não é o conector PS2 que se tinha nos anos 90. É um cabo telefônico de par trançado com conector RJ-11, que é o mesmo cabo e conector que se usava em telefones nos anos 80. Eu não cheguei a usar esses terminais, mas acho que eles literalmente &ldquo;discavam&rdquo; pro computador pra se conectar, mais ou menos como se fazia com modems discados.</p>
<p>Na época já existia cabos coaxiais, que é parecido com da sua TV a cabo ou cabo de áudio, aquele mais grosso, que tem um canal físico no meio insulado por uma camada que é uma blindagem. A camada do meio transporta o sinal e é envolto por outro canal físico que é criado pelo plástico. O canal de fora é o ground, ou terra. São bons cabos mas caros.</p>
<p>A alternativa surge com o que chamamos de twisted pair ou par trançado, que contém literalmente um par de fios de cobre, normalmente com cores diferentes, trançados um no outro. Lembram das aulas de física elétrica no colegial? Se você tem um cabo por onde passa uma corrente, o que acontece? Ele gera um campo eletromagnético. Se fez cursinho até deve lembrar do truque do dedão, onde colocamos o dedão na direção da corrente e os outros dedos representam o campo eletromagnético.</p>
<p>O problema de cabos não trançados é que quando eles são expostos a um campo magnético, formam um loop e indução de voltagem. Os circuitos no final do cabo ficam mais suscetíveis a barulho induzido se não estiverem trançados. Um circuito é formado por dois cabinhos, daqueles coloridos, com voltagens opostas. Se um fio carrega voltagem positiva, a outra é igual mas negativa. A voltagem das duas é a mesma mas com polaridades opostas. Sem entrar em detalhes, ao ter dois cabos passando a mesma amplitude e frequencia mas de polaridades opostas trançados entre si, isso cancela a interferência eletromagnética, garantindo uma taxa de sinal melhor e menos barulho.</p>
<p>Agora começa a parte importante. Você pode carregar múltiplos canais num mesmo meio físico, como os fios de cobre. Isso se chama multiplexing e é um tópico beeeem complicado. Na prática pense em várias conversas de voz que podem ser combinadas num único sinal, esse é o processo de multiplexing ou mux e tem vários jeitos de fazer isso. Lembrem voz, música é tudo onda, frequências e amplitudes que são transmitidos como sinais elétricos.</p>
<p>Mux pode ser feito com divisão de espaço ou de frequência ou de tempo ou polarização e por aí vai. O importante é saber que é possível combinar múltiplos canas num único sinal e transferir tudo junto num mesmo meio físico, no caso os cabos. E do outro lado, na central, é possível fazer demultiplexing ou demux e separar os canais. É assim que rede telefônica funciona e esse processo não é novo, foi inventado no fim do século 19.</p>
<p>Quando falamos do termo bandwidth ou banda, estamos falando do máximo teórico de faixas de frequência que o meio físico permite passar. Isso determina quantos canais podem passar pelo mesmo cabo. Muito abaixo ou muito acima da frequência que o meio aguenta e o sinal é degradado. Daí você cai em coisas como cálculo de capacidade de canal de Shannon-Hartley. Claude Shannon, que todo mundo de ciências da computação, conhece como o pai da Teoria da Informação, que justamente criou a matemática pra calcular esse tipo de coisa.</p>
<p>Enfim, tem muita matemática e física envolvida em telecomunicações e eu só passei muito rápido por cima, mas eu queria chegar no ponto do telefone de copinho de novo. Com tudo isso, no final o que temos é um modelo mais sofisticado pro mesmo conceito de telefone de copinho. Onde uma vez estabelecido uma sessão entre dois participantes, você trava o canal de comunicação de ponta a ponta só pra vocês. Por isso o modelo de negócio era tarifar por minuto onde esse canal ficava preso. Isso é comutação de circuito.</p>
<p>Agora, ondas são super versáteis. Não é só conversa de voz ou música. Podemos converter dados binários em ondas. E de novo, é um tanto de física e matemática, mas falando de forma super simplificada, podemos usar a amplitude pra representar quando é bit 1 ou bit 0 e a frequência pra determinar quantos bits podem trafegar por segundo. O processo de transformar bits digitais em ondas é o que se chama de modulação, e o processo inverso pra recuperar esses bits é a demodulação e por isso os equipamentos que fazem isso são chamados de &ldquo;modem&rdquo;.</p>
<p>Com os primeiros modems comerciais, a gente conectava a linha telefônica de casa num modem e discava pra um provedor. No provedor ele ia ter dezenas de linhas telefônicas com modems também. Quando discava pra lá um modem atendia e travava tanto minha linha telefônica quanto a do provedor, fechando o circuito.</p>
<p>Imagina como isso era caro. Antes da privatização das telecomunicações, ter uma linha telefônica era caríssimo, se bobear mais caro que um carro, era literalmente um investimento. Acho que foi logo depois que privatizaram e linhas telefônicas começaram a ficar acessíveis que possibilitou ter provedores com dezenas ou centenas de linhas pra atender as pessoas, mas mesmo assim, se quisesse ter 100 pessoas simultaneamente conectadas precisava ter 100 linhas, era bem difícil de escalar e era super comum ligar pro provedor e dar ocupado.</p>
<p>Sem contar que era inconveniente pras pessoas. Toda vez que discava e conectava a linha ficava ocupada, então não dava pra receber chamadas nem ligar pra ninguém. Um dos motivos de porque a gente só entrava na internet de noite e de madrugada. Fora que tinha aquele esquema acho que era da noite de sábado e domingo só cobrava um pulso então não saia tão caro. Pois é, era difícil isso de acessar a internet nos anos 90. Você tinha que se planejar. Mas felizmente a tecnologia avançou rápido.</p>
<p>Uma característica da voz falada humana é que a gente não usa todas as frequências que um cabo de telefone suporta. A gente pensa em cabos e fala em bits por segundo. Mas simplificando, lembra que bits são modulados em ondas por um modem. O cabo na verdade tem uma capacidade de frequências e por isso falamos em cabos digamos de 1.5 megahertz e male male arredondamos pra 1.5 megabits.
A conversão não é direta assim, mas pra hoje pensa que é relacionado. Enfim, digamos que o cabo telefônico tenha uma banda de 1.5 megahertz. A voz humana não precisa de uma faixa muito maior que de 0 até 4 kilohertz, então poderíamos reservar só essa faixa pra voz e vai sobrar muitos outros canais até 1.5 megahertz.</p>
<p>Significa que quando travamos o circuito pra uma ligação, estamos desperdiçando toda essa banda numa conversa. Daí nasce a era dos modems ADSL como da Speedy que funcionava na mesma rede telefônica e permitia deixar o canal de voz livre pra ligações e conectar na internet com o mesmo cabo. No antigo sistema CAP, de 0 até 4 kilohertz era reservado pro canal de voz, daí de 25 a 160 kilohertz era o canal de upstream, pra upload de dados. O canal downstream pra download começava em 240 kilohertz e ia até a banda máxima do cabo que podia ser 1.5 kilohertz. Tem um espaço vazio de frequências entre cada canal pra diminuir interferência.</p>
<p>O sistema CAP mudou rápido pra DMT que é discrete multitone e em resumo ela divide as frequências em vários pequenos canais de 4 kilohertz, um total de 247 canais separados. Lembra quando falei que antes um provedor precisava ter uma linha telefônica pra atender cada um usuário? Pensa agora uma única linha podendo atender um máximo de 247 canais na mesma linha. Se um canal estivesse com muito ruído, o switch mudava pra outro canal. Não quero entrar em detalhes nisso porque eu nunca trabalhei numa telecom nessa área então vai ter muito detalhe que não vou saber. Mas imagina que isso evoluiu pro modem da TV a cabo ou da fibra ótica da Vivo que eu uso.</p>
<p>Mais importante é que mudamos do conceito de circuit switching pra packet switching. Agora vocês precisam entender pelo menos o conceito de frames e protocolos. O jeito antigo é o que podemos chamar de ponto a ponto, inclusive o protocolo de provedor discado era usando um protocolo chamado PPP que é Point to Point Protocol, tinha um outro mais antigo chamado SLIP. Quem usou internet no começo dos anos 90 vai lembrar disso.</p>
<p>O modem faz o handshake, aquele barulhinho nostálgico que povo da minha idade conhece, o aperto de mão com o outro modem do outro lado e eles abrem uma conexão stateful, uma sessão exclusiva fechada. A partir daí está aberto um stream de dados, que pode ser half duplex ou full duplex. Uma via de mão única ou uma via com duas faixas, uma pra download e outra pra upload.</p>
<p>Uma vez aberto um stream, se está fazendo download, inicia o recebimento e espera até acabar tudo e não faz mais nada enquanto isso. Se está enviando, começa a enviar, e espera até acabar. Que nem numa chamada de voz se não tem tráfego nenhum, mesmo assim a sessão fica aberta e sendo desperdiçada até um dos lados desligar. Mas isso é um desperdício. Em vez disso podemos pegar qualquer informação, seja um email, seja um arquivo, seja um comando ou o que for e fracionar essa informação em blocos de tamanhos fixo. Lembra o linguição de bits que eu expliquei em outros episódios, e lembra o conceito de que a menor unidade na partição do seu HD não são arquivos e sim blocos? É parecido.</p>
<p>Muita gente que só usou interface gráfica em sistema operacional tem a noção que a menor unidade é sempre um arquivo. E que uma partição é basicamente como o &ldquo;C:&rdquo; que você vê no Explorer. Por isso fiz uma playlist inteira pra explicar sobre armazenamento e como dados são realmente organizados num disco. Todo HD ou pendrive é o que chamamos de dispositivos de bloco. E em telecomunicações é parecido, mas a menor unidade chamamos de pacote ou datagram. E datagram pra não esquecer é só pensar que no mundo analógico temos telegrams ou telegramas. Datagram é o telegrama de sinais digitais.</p>
<p>Se tava achando complicado, a partir daqui que realmente vai complicar. Então antes de continuar eu realmente preciso que vocês tenham na cabeça essa idéia de fragmentar informação em pacotes. Pensa assim. Todo dado, seja um arquivo ou um email, é um linguição de bits. Expliquei bastante sobre isso no episódio sobre a diferença de arquivo texto e binário. Se temos aquele arquivo mp3 de 10 megabytes, são 80 megabits ou mais ou menos 80 milhões de zeros de uns um atrás do outro que formam o conteúdo desse arquivo.</p>
<p>Na mesma analogia do telefone de copinho, se formos transferir esse arquivo tudo de uma só vez, os dois lados precisam esperar essa operação acabar pra conseguir enviar ou receber outros arquivos. E no modelo de circuito fechado, se vocês ficarem conectados o dia todo, mesmo se não estiver passando nenhum dado, a conexão vai ser desperdiçada. Por isso os sistemas de telecomunicação mudaram do modelo de circuit switch pra packet switch e de sessões stateful pra stateless. É isso que permite a escalabilidade que temos agora. Estou resumindo de forma bem grosseira, mas vamos entender o que muda.</p>
<p>A partir de agora cada canal não fica mais travado entre dois pontos. Está todo mundo conectado sem travar uma sessão, por isso chamamos de stateless, sem estado. Agora eu quero fazer download daquele mp3, eu mando o comando pra algum servidor na outra ponta e ele vai começar a me mandar esse arquivo. Só que não vai mandar tudo de uma só vez. Em vez disso ele vai quebrar esse arquivo em pacotes. Digamos, pedaços de tamanho fixo de 1500 bytes. Isso vai dar um total de mais ou menos 7 mil pedaços. Mas não é só isso.</p>
<p>Imagine um sistema de correios ou DHL e esses pedaços precisam ser entregues. Como faz? Você coloca cada pedaço de informação numa caixa e preenche os dados do destinatário. No caso da internet vamos dizer que é o endereço de IP da pessoa que pediu. Além disso o servidor coloca outros metadados no pacote, como o número do pedaço, porque quando eu for recebendo, vou precisar remontar o arquivo pedaço a pedaço mas precisa ser na ordem certa, senão o arquivo seria corrompido.</p>
<p>Agora o servidor começa a enviar esses pacotes, um a um. Toda vez que eu receber, eu devolvo uma resposta, um acknowledgement ou ACK, dizendo &ldquo;beleza, manda o próximo&rdquo;. Se durante a transmissão teve alguma interferência no cabo ou algo assim, e o pacote for danificado, quando eu recebo consigo checar. Existe toda uma forma de reconhecimento de erros que vou explicar no próximo episódio, só assuma que eu sei como reconhecer se o pacote veio amassado e aí eu jogo fora e respondo &ldquo;não veio direito, pode mandar outro&rdquo;, esse é o NAK, o oposto de ACK, daí o servidor repete o mesmo pacote e envia, e vai fazendo isso pacote a pacote.</p>
<p>Agora digamos que um segundo usuário aqui em casa na mesma rede resolva assistir episódio novo de série na Disney+. Se fosse que nem antigamente, não ia conseguir, ia precisar esperar eu terminar meu download. Mas o lance é que no intervalo entre um pacote e outro do meu mp3, ela pode mandar outros pacotes pra outros lugares no mesmo canal. O grande lance é que um cabo de comunicação era uma rodovia de várias faixas, ou canais como a gente chama, que só transitava um carro de cada vez. Quando fechava o circuito eu tava fechando e monopolizando essa rodovia inteira só pra mim. Mas agora a rodovia fica aberta e a gente vai usando as faixas que estão livres pra transitar mais carros.</p>
<p>De novo, é uma explicação grosseira, mas o conceito é que em vez de monopolizar um circuito, agora a gente compartilha, fragmentando a carga em vários pedaços. Daí mesmo que todas as faixas estejam ocupadas, na mesma faixa pode ir vários pedaços de vários arquivos. Como pacotes sendo enviados pelo Mercado Livre, cada pacote com seu endereço e destinatário pra não misturar e cada um recebendo seu pacote no final direitinho. É como se eu quisesse comprar um PC na Amazon. Eu compro cada componente separado. Eles são entregues fora de ordem, mas com o manual correto, eu consigo remontar o PC direitinho no final. E cada pacote veio de um lugar diferente por rotas diferentes até. A internet funciona mais ou menos assim.</p>
<p>Mas eu estou me adiantando. Até aqui eu quis descrever em linhas gerais pra que serve esse cabo de fibra ótica ou o cabo de TV a cabo que chega até meu modem e porque é possível ter várias pessoas no meu prédio usando o mesmo cabo, em diferentes canais. Esse é o papel do modem, modular e demodular o sinal digital em sinal elétrico, e trafegar em canais de frequência por cabos de cobre ou fibra até o switch de alguma central, no caso do provedor, e de lá ir pra internet. Agora que começa a parte interessante.</p>
<p>Por acaso, esse modem da Vivo, assim como modems de outros provedores como a Claro, tem múltiplas funções. É ao mesmo tempo um modem, mas acumula funções de um roteador e ponto de acesso Wifi. E eles costumam ser ruins em todos eles. Pra poucos dispositivos e pouco uso, realmente não faz nenhuma diferença usar. Mas no meu caso eu prefiro desligar essas funções e conectar num roteador separado, no caso esse meu TP-Link aqui do lado.</p>
<p>Mas acho que por hoje já é suficiente e vou pausar a história por aqui. No próximo episódio quero resumir pra que serve um roteador e como ele distribui a conexão pro meu PC, TV, e tudo mais. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e compartilhem o video com seus amigos. A gente se vê, até mais.</p>
]]></content:encoded><category>redes</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #117 - Linguagem Compilada vs Interpretada | Qual é melhor?</title><link>https://akitaonrails.github.io/2022/04/15/akitando-117-linguagem-compilada-vs-interpretada-qual-e-melhor/</link><guid isPermaLink="true">https://akitaonrails.github.io/2022/04/15/akitando-117-linguagem-compilada-vs-interpretada-qual-e-melhor/</guid><pubDate>Fri, 15 Apr 2022 17:34:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/SNyh-cubxaU"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Chegou a hora de finalmente entender qual a diferença de linguagens compiladas e interpretadas, linguagens estáticas e dinâmicas. Java é compilado? Javascript é interpretado? Qual a diferença?&lt;/p&gt;
&lt;p&gt;Hoje você vai ganhar uma fundação mais sólida pra entender linguagens da maneira correta e é o pré-requisito pros próximos videos onde finalmente vou discutir as linguagens mais famosas da atualidade.&lt;/p&gt;
&lt;p&gt;== Conteúdo&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;00:00:00 - Intro&lt;/li&gt;
&lt;li&gt;00:00:55 - Pré-Requisitos&lt;/li&gt;
&lt;li&gt;00:01:53 - Hello World em C e Java&lt;/li&gt;
&lt;li&gt;00:03:03 - ELF vs CAFE&lt;/li&gt;
&lt;li&gt;00:03:41 - 1a tentativa: compilador vs interpretador&lt;/li&gt;
&lt;li&gt;00:04:45 - Estudo de Linguagens&lt;/li&gt;
&lt;li&gt;00:07:37 - Análise Léxica&lt;/li&gt;
&lt;li&gt;00:10:59 - Análise Sintática&lt;/li&gt;
&lt;li&gt;00:14:39 - Abstract Syntax Tree (AST)&lt;/li&gt;
&lt;li&gt;00:15:48 - Notação Polonesa&lt;/li&gt;
&lt;li&gt;00:17:58 - Otimização de Bytecode&lt;/li&gt;
&lt;li&gt;00:22:55 - Pra que serve um Programador?&lt;/li&gt;
&lt;li&gt;00:27:04 - Linters&lt;/li&gt;
&lt;li&gt;00:28:00 - Backus, Naur, BNF e História&lt;/li&gt;
&lt;li&gt;00:31:57 - Parsers e &amp;ldquo;DOM&amp;rdquo;&lt;/li&gt;
&lt;li&gt;00:32:58 - Interpretadores e Máquinas Virtuais&lt;/li&gt;
&lt;li&gt;00:36:56 - Linguagens Dinâmicas&lt;/li&gt;
&lt;li&gt;00:38:58 - Otimização Binária&lt;/li&gt;
&lt;li&gt;00:42:52 - As Fases de um Compilador&lt;/li&gt;
&lt;li&gt;00:46:23 - Just-In-Time Compiler (JIT)&lt;/li&gt;
&lt;li&gt;00:50:33 - Linkers&lt;/li&gt;
&lt;li&gt;00:59:24 - JIT de novo&lt;/li&gt;
&lt;li&gt;01:02:30 - Google V8&lt;/li&gt;
&lt;li&gt;01:05:25 - Por que dinâmico em vez de estático?&lt;/li&gt;
&lt;li&gt;01:07:42 - 2a tentativa: compilador vs interpretador?&lt;/li&gt;
&lt;li&gt;01:10:45 - Bônus: Bloopers (novidade)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;== Links&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/SNyh-cubxaU"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Chegou a hora de finalmente entender qual a diferença de linguagens compiladas e interpretadas, linguagens estáticas e dinâmicas. Java é compilado? Javascript é interpretado? Qual a diferença?</p>
<p>Hoje você vai ganhar uma fundação mais sólida pra entender linguagens da maneira correta e é o pré-requisito pros próximos videos onde finalmente vou discutir as linguagens mais famosas da atualidade.</p>
<p>== Conteúdo</p>
<ul>
<li>00:00:00 - Intro</li>
<li>00:00:55 - Pré-Requisitos</li>
<li>00:01:53 - Hello World em C e Java</li>
<li>00:03:03 - ELF vs CAFE</li>
<li>00:03:41 - 1a tentativa: compilador vs interpretador</li>
<li>00:04:45 - Estudo de Linguagens</li>
<li>00:07:37 - Análise Léxica</li>
<li>00:10:59 - Análise Sintática</li>
<li>00:14:39 - Abstract Syntax Tree (AST)</li>
<li>00:15:48 - Notação Polonesa</li>
<li>00:17:58 - Otimização de Bytecode</li>
<li>00:22:55 - Pra que serve um Programador?</li>
<li>00:27:04 - Linters</li>
<li>00:28:00 - Backus, Naur, BNF e História</li>
<li>00:31:57 - Parsers e &ldquo;DOM&rdquo;</li>
<li>00:32:58 - Interpretadores e Máquinas Virtuais</li>
<li>00:36:56 - Linguagens Dinâmicas</li>
<li>00:38:58 - Otimização Binária</li>
<li>00:42:52 - As Fases de um Compilador</li>
<li>00:46:23 - Just-In-Time Compiler (JIT)</li>
<li>00:50:33 - Linkers</li>
<li>00:59:24 - JIT de novo</li>
<li>01:02:30 - Google V8</li>
<li>01:05:25 - Por que dinâmico em vez de estático?</li>
<li>01:07:42 - 2a tentativa: compilador vs interpretador?</li>
<li>01:10:45 - Bônus: Bloopers (novidade)</li>
</ul>
<p>== Links</p>
<ul>
<li>ANSI C Grammar (<a href="https://www.lysator.liu.se/c/ANSI-C-grammar-y.html#compound-statement"target="_blank" rel="noopener">https://www.lysator.liu.se/c/ANSI-C-grammar-y.html#compound-statement</a>)</li>
<li>EcmaScript 2023 (<a href="https://tc39.es/ecma262/#sec-ecmascript-language-statements-and-declarations"target="_blank" rel="noopener">https://tc39.es/ecma262/#sec-ecmascript-language-statements-and-declarations</a>)</li>
<li>List of Java bytecode instructions (<a href="https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions</a>)</li>
<li>Why the New V8 is so Damn Fast (<a href="https://nodesource.com/blog/why-the-new-v8-is-so-damn-fast/"target="_blank" rel="noopener">https://nodesource.com/blog/why-the-new-v8-is-so-damn-fast/</a>)</li>
<li>V8 Bytecode.h (<a href="https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h"target="_blank" rel="noopener">https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h</a>)</li>
<li>LLVM Analysis and Transform Passes (<a href="https://llvm.org/docs/Passes.html"target="_blank" rel="noopener">https://llvm.org/docs/Passes.html</a>)</li>
<li>HHVM (<a href="https://hhvm.com/"target="_blank" rel="noopener">https://hhvm.com/</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Todo bom iniciante que se preza mais cedo ou mais tarde vai se engajar numa discussão sobre porque sua linguagem favorita é melhor que dos outros. E invariavelmente um dos argumentos que vai aparecer é que ser compilado é melhor, é mais performático, ou que ser interpretado é melhor porque é mais produtivo. E certamente todos que estão participando dessa discussão estão errados de maneiras que vocês nem imaginam.</p>
<p>Hoje quero falar um pouco sobre o que significa uma linguagem ser compilada ou interpretada e porque a maioria dos programadores interpreta isso errado (no pun intended). O objetivo não de hoje não é dizer qual é melhor, e sim porque sua linha de raciocínio não leva em conta diversos outros fatores que são mais importantes. No final do video você vai enxergar todas as suas linguagens de uma forma diferente.</p>
<p>(&hellip;)</p>
<p>Pra todo mundo que tá estudando ou já se formou em ciências da computação: não, eu não vou falar do livro do dragão de compiladores. Também não vou falar do livro de sistemas operacionais do Tanenbaum. Porém todo mundo que estudou essas duas matérias vai ter um aproveitamento melhor desse video. Aproveitem pra discutir os detalhes técnicos com seus colegas ou fazer mais perguntas pros seus professores na faculdade e nos cursos.</p>
<p>Compiladores e sistemas operacionais são conteúdos pra pelo menos um ano ou mais no curso de ciências da computação. Partes desses assuntos eu já expliquei em videos como o guia mais hardcore de introdução à computação e hello world como você nunca viu antes, e todos os outros na playlist de &ldquo;Como Seu Computador Funciona&rdquo;. Mas pra ter uma idéia, na faculdade, um dos trabalhos é fazer seu próprio compilador. Se nunca fez, depois do video fica de desafio tentar fazer o seu próprio. E pra isso já dou spoiler pra pesquisar tutoriais sobre a ferramenta ANTLR. Mas não vou falar especificamente de ANTLR hoje também.</p>
<p>Vamos começar do começo. Isso aqui é um Hello World em C. Agora, isso aqui é um terminal de Linux no WSL2 do meu PC e compilamos esse arquivo texto com o código em C num binário ELF compatível com Linux usando o compilador &ldquo;cc&rdquo;, que significa &ldquo;Compiler Collection&rdquo;. E pronto, isso é tudo que a maioria tem na cabeça quando se pensa em transformar código texto em um executável e no video sobre arquivos texto e binários eu explico como realmente as coisas funcionam e como um executável funciona. Mas vamos pra outro exemplo.</p>
<p>Agora, isso aqui é um Hello World em Java. Novamente vamos pro Terminal e é assim que se aprende na faculdade, roda <code>javac Hello.java</code> e ele vai gerar um <code>Hello.class</code>. E pra rodar é só fazer <code>java Hello</code> e voilá. Muita gente costuma dizer que Java é uma linguagem &ldquo;compilada&rdquo;, entre aspas. Só que olha o que estamos fazendo: chamando o executável &ldquo;java&rdquo; antes e dando como parâmetro a classe &ldquo;Hello&rdquo; que acabamos de compilar. Se fosse realmente compilado, no sentido clássico, deveria dar pra só fazer <code>./Hello.class</code>, mas isso não funciona, o sistema operacional não tem nenhuma idéia de como executar isso, porque um binário <code>.class</code> não é um executável em formato ELF.</p>
<p>Como tinha explicado no video de arquivos, pro Linux saber que um binário pode ser executado, ele precisa estar num formato chamado ELF, que significa Executable and Linkable Format. Em particular, todo binário ELF começa com a sequência que em hexadecimal seria 7f 45 4c 46, que propositalmente é ASCII pra string &ldquo;ELF&rdquo;. Isso é totalmente arbitrário e quem inventou o ELF só definiu que é assim e pronto. Mas se abrirmos os primeiros bits da classe Hello de Java, o que vemos nos primeiros bytes é a sequência hexadecimal <code>cafe babe</code>. Obviamente uma piada de café e java que o James Gosling escolheu pra serem os primeiros bytes de todas as classes Java.</p>
<p>Se definirmos que compilar é o processo de pegar um código fonte texto, por exemplo em C, passar por uma ferramenta como o &ldquo;cc&rdquo;, que vai cuspir um binário que o sistema operacional vai conseguir executar diretamente, então não, o Java não é compilado. Por outro lado vamos definir um interpretador. Um interpretador é um programa que vai pegar ou o arquivo texto do código ou uma representação intermediária, como esse &ldquo;.class&rdquo; e vai traduzir pro binário de máquina que, aí sim, o sistema operacional e o processador vão entender. Portanto, por essa definição, Java é uma linguagem interpretada.</p>
<p>Isso é o que você vai encontrar na bolhadev, e na verdade o que acabei de falar não tá nem totalmente certo, nem totalmente errado. Tanto C quanto Java são linguagens compiladas. E antes de explicar qual a diferença, preciso começar explicando o que é o processo de compilação. Esse processo tem diversos passos mas minha intenção não é escovar bits hoje, mas sim fazer com que vocês tenham um modelo mental fácil na cabeça pra não cairem em pegadinhas de discussão online e conseguir entender as principais diferenças.</p>
<p>O estudo de linguagens de programação é meio que um sub-conjunto do estudo de linguagens em geral, tipo português ou inglês. Em ciência da computação, na teoria formal de linguagens, as linguagens de programação como C ou Java ou Python são o que chamamos de linguagens regulares. Por sua vez, uma linguagem regular é uma linguagem formal que pode ser definida por expressões regulares, sabe o regex que você usa todo dia pra validar formato de emails ou cpfs? E por sua vez uma expressão regular pode ser definida como uma linguagem que pode ser reconhecida por um autômato finito. Älguns de vocês já devem ter esbarrado no termo &ldquo;máquinas de estado finito&rdquo;, que é outro nome que se dá a autômatos de estado finito.</p>
<p>Mas relaxa, nem eu sei tudo em todos os detalhes e não vou descer na matemática por trás de autômatos não, mas é pra saber que existe esse campo de estudo. Na prática, pense assim. Temos um texto em português. O que é um texto? É uma coleção de parágrafos. E o que são parágrafos? É uma coleção de frases. E o que é uma frase? É uma coleção de palavras. Palavras são coleções de letras separadas por espaço ou outras pontuações, como vírgula ou ponto.</p>
<p>Então, se eu juntar quaisquer letras, vou ter palavras? Se eu juntar quaisquer palavras, vou ter frases? Não. Existe uma ordem que funciona. Conjuntos de letras só formam palavras se elas existem num dicionário, que é uma coleção arbitrárias de conjuntos de letras, que a gente definiu. Se eu juntar as letras &ldquo;h&rdquo;, &ldquo;e&rdquo;, &ldquo;l&rdquo;, &ldquo;l&rdquo;, &ldquo;o&rdquo;, um atrás do outro, no dicionário em inglês, isso define uma saudação e uma forma de iniciar uma comunicação por telefone, por exemplo. Tire um &ldquo;l&rdquo; dessa palavra e já não significa a mesma coisa.</p>
<p>Pra formar frases, não pode sair juntando quaisquer palavras. Elas precisam obedecer uma gramática. Sujeito, predicado, adjetivos, pronomes, tempos verbais e tudo mais que fazem uma frase ter sentido. Se eu disser a frase &ldquo;não vai ter jeito&rdquo;, significa que não tem mais conserto, fodeu já. Mas se na verdade faltou uma vírgula nessa frase depois do não, então seria &ldquo;não, vai ter jeito&rdquo;, que é exatamente o significado oposto. Uma única vírgula faz diferença. Quem já programou e teve bug porque esqueceu uma vírgula, em português, também tem bug se esquecer uma pontuação.</p>
<p>Voltando pro código C. Pro computador, um texto, é só um linguição de bytes, um linguição de caracteres. Tem letras, tem espaços, tem chaves, mas não quer dizer nada. Mesmo pra você, ser humano assistindo, se nunca programou, esse texto também não quer dizer bulhufas. Eu preciso dar um jeito de fazer o computador juntar letras que formam palavras, que no caso chamamos de &ldquo;tokens&rdquo;, que fazem sentido. &ldquo;int&rdquo; é um conjunto de letras que faz sentido. &ldquo;printf&rdquo; é outro conjunto de letras que faz sentido. Então preciso conseguir quebrar esse texto todo em uma lista de tokens. Pra isso vou &ldquo;tokenizer&rdquo;, ou &ldquo;lexar&rdquo;, e isso é o trabalho de uma ferramenta que faz análise léxica.</p>
<p>Na realidade precisamos definir os lexemes da linguagem, o equivalente de dicionário. Podemos definir que dígitos são todos os caracteres de zero até 9. E definimos como não-dígitos, todas os caracteres de &ldquo;a&rdquo; até &ldquo;z&rdquo;, em mínusculo e maiúsculo. Também definimos que pontuação são todos estes outros caracteres como parênteses, colchetes, vírgula, ponto e tudo mais. Alguns desses caracteres podem repetir em outra definição. Parênteses podem ser operadores, assim como sinal de mais, de menos, asterisco. E assim por diante. Agora um analisador léxico como um chamado <code>flex</code> pode pegar meu código em C e entender que tem sinais, strings, digitos, operadores e traduzir isso numa lista de tokens.</p>
<p>Uma vez tendo essa lista de tokens, precisamos saber o que significam. Eu preciso que o computador entenda que, o que vai entre parênteses depois do token &ldquo;printf&rdquo;, é um argumento. Eu preciso que ele entenda que o &ldquo;return 0&rdquo; na última linha entre chaves se refere ao tipo &ldquo;int&rdquo; declarado no começo da função &ldquo;main&rdquo;. Elas precisam estar seguindo alguma regra. Uma vez tendo uma lista de tokens, precisamos checar contra uma gramática.</p>
<p>Antes de ficar teórico demais, deixa eu mostrar um exemplo prático. Pra isso fiz a linguagem de programação mais idiota do mundo. Ela não é turing complete, só aceita uma expressão no formato <code>1 add 2</code> pra somar, ou <code>4 sub 3</code> pra substrair. Só isso. Olha só, escrevi um programa nessa linguagem e salvei como &ldquo;hello.stupid&rdquo;, por que não? Pra rodar esse programa, fiz um interpretador em Javascript chamado &ldquo;js stupid&rdquo;. Vamos abrir pra ver o que faz.</p>
<p>As primeiras linhas é o jeito mais porco de pegar um argumento da linha de comando. Vou executar fazendo &ldquo;./stupid.js hello.stupid&rdquo;, igual o Java faz com &ldquo;java Hello&rdquo; antes, lembra. Pra isso botei um shebang na primeira linha que vai executar o Node automaticamente e carregar o resto do script nele. Depois pego o primeiro argumento e uso a biblioteca <code>path</code> pra formar o caminho absoluto até esse arquivo.</p>
<p>Daí uso a biblioteca &ldquo;fs&rdquo;, de filesystem, pra ler o arquivo. Se eu passar um arquivo que não existe vai dar um erro e sair. Senão eu pego o conteúdo do arquivo, limpo coisas no final como o caracter de nova linha, caso tenha, sabe o &ldquo;\n&rdquo;? E aí simulo o que seria o trabalho do tal analisador léxico, que vai tokenizar o conteúdo do arquivo.</p>
<p>Meu lexer é tão simples que é só um split de string de javascript. Ele quebra onde tem espaço e joga os tokens nesse array que chamei de &ldquo;tokens&rdquo;. Nesse ponto, converti o texto do arquivo numa lista e ainda converti o primeiro e o último token pra serem números.</p>
<p>Essa minha linguagem é tão besta que não precisa de um analisador sintático. Eu só faço um switch case e vejo o 2o token da lista. Se for &ldquo;add&rdquo; eu faço uma soma, se for &ldquo;sub&rdquo; eu faço uma subtração. E é isso aí. Eu posso adicionar quantas operações quiser seguindo esse formato. E no final, se colocar um operador que não tá declarado, vai dar erro de sintaxe e sair.</p>
<p>Se tiver um pouquinho de imaginação vai conseguir criar linguagens bem simplinhas usando só esse esquema. Mas dando spoilers já adianto onde que vai dar nó na cabeça. E se eu quisesse suportar uma expressão com mais de 2 números? Tipo fazer 1 + 2 + 3, como você faria? E se eu quisesse adicionar multiplicação e quiser que 1 + 2 * 3 dê a resposta correta? Note que multiplicação tem precedência, então 2 * 3 tem que vir antes de somar por 1. E dá pra ir complicando. E se eu quiser suportar variáveis? E se eu quiser suportar funções?</p>
<p>Obviamente isso é um problema já resolvido. E o primeiro conceito é separar a análise de sintaxe da execução propriamente dita, que é o que costumamos chamar de &ldquo;tempo de compilação&rdquo; e &ldquo;tempo de execução&rdquo;. Recapitulando, a análise léxica só dividiu o texto em tokens, e agora análise sintática vai dar significado pra esses tokens. Pra isso vai precisar de uma gramática que define o que são expressões, o que são funções, o que são condicionais. Por exemplo, podemos ver a gramática da linguagem C.</p>
<p>As ferramentas tradicionais que aprendemos na faculdade são lex ou flex, pra definir a análise léxica, e o bison ou yacc pra definir a gramática. Se procurar no GitHub é fácil de achar a gramática de todas as linguagens. A gramática do C em yacc é bem curtinha. Você imagina que definir uma linguagem deve ser milhares de linhas, mas na realidade não é mais que meia dúzia de page down, algumas centenas de linhas. Isso não é o compilador todo, só o parser, claro.</p>
<p>Se olharmos lá no fim da gramática, eis a definição do que é uma função de C, literalmente ele nomeia como &ldquo;function definition&rdquo;. E pode ser construído de 4 formas diferentes. A 1a são especificadores de declaração, seguido de declarador, seguido de lista de declaração e terminando com composição de statements. Em português não sei como chamam &ldquo;statements&rdquo;, alguns chamam de expressão, mas a definição de expression e statement são diferentes. Eu acostumei a chamar de statement então vai assim mesmo.</p>
<p>Mas o que tudo isso quer dizer? Esses nomes são como etiquetas, a definição de cada uma tá mais pra cima. Por exemplo, vamos ver o que &ldquo;compound statement&rdquo; quer dizer. Olha só, pode ser também 4 coisas: um bloco entre chaves vazias, ou uma lista de statements entre chaves, ou uma lista de declarações entre chaves e por último pode ser uma lista de declarações seguida de uma lista de statements, mas o principal é que estamos olhando pra declaração oficial, completa e não-ambígua do que o C chama de &ldquo;função&rdquo;. Não há espaço pra discussão, esta é gramática que define o C.</p>
<p>Agora o que é um statement list? Aqui vemos a definição e ela pode ser um statement ou vários statements, isso é meio que uma definição recursiva, por ser ela mesma várias vezes. Mas isso só define que pode ter um ou mais statements, agora precisamos ver o que é um statement. E agora sim, um statement pode ser de diversos tipos como labeled statement, compound statement que vimos antes, mas o interessante vai ser esse iteration statement.</p>
<p>E olha só, é isso que C chama de statements de iteração. Pode ser um while, pode ser um do while, pode ser for. E o for pode ser de dois jeitos diferentes, com ou sem uma expressão no final. E assim por diante. Se você tiver paciência pra ler a definição léxica e a gramática de yacc, tecnicamente é toda a sintaxe e semântica do C definida em 2 arquivos razoavelmente curtos, dá pra decorar e saber de cabeça. Claro, não é um iniciante que vai ler esses arquivos e entender tudo. Mas se alguma vez você já se perguntou, onde que tem a definição exata de uma linguagem? São nesses arquivos, e não em blog post.</p>
<p>Por exemplo, o Javascript tem uma definição semelhante. No site do EcmaScript podemos ver como é definido a versão mais nova 2023. E se pularmos aqui no menu pra Notação de Gramática, olha só como é a definição de um Variable Declaration, ou declaração de variável: é um Binding Identifier e um Initializer. Um Binding Identifier são tokens como &ldquo;const&rdquo;, &ldquo;let&rdquo; ou &ldquo;var&rdquo; e um Initializer é uma expressão de assignment como &ldquo;x = 1&rdquo;. E olha aqui embaixo, ele define como é um loop com for, um For Statement, que pode ser definido de 4 maneiras diferentes, todos com um Lexical Declaration e variando a forma da Expression.</p>
<p>Mas beleza, essa é a gramática mas e daí, pra que serve? O objetivo é quebrar seu código fonte, que é um texto, em uma listona de tokens e depois usar a gramática pra organizar esses tokens em uma estrutura de dados que podemos manipular programaticamente. O objetivo é transformar seu código em uma árvore, mais especificamente uma Parse Tree. Lembram? Eu sempre falo que uma das estruturas de dados mais importantes é uma árvore, por isso dediquei um video inteiro só sobre isso. No final seu código fonte vai ficar mais ou menos assim: (imagem)</p>
<p>Aliás, tudo que expliquei até agora sobre analisadores léxicos e sintáticos, o processo de transformar um texto de código em tokens e reorganizar esses tokens numa árvore, é o que muita gente chama de &ldquo;parser&rdquo;, mas na realidade a primeira etapa de tokenizar é feito por um lexer, e a segunda parte de pegar os tokens e transformar em árvore é feita por um parser. Pra simplificar, vou pular a explicação do que é uma Parse Tree e ir direto pra mostrar outra representação da mesma árvore, que é a árvore de sintaxe, Syntax Tree, ou mais especificamente uma Abstract Syntax Tree ou AST.</p>
<p>Lembra daquela minha linguagem de programação mais estúpida do mundo, que só aceita uma linha e só faz soma de dois elementos? Digamos que eu evolua ela pra fazer conta com mais de dois elementos e queira escrever aquele <code>1 + 2 * 3</code>. Essa é a notação que chamamos de infixa onde operadores como o sinal de mais e o asterisco vão no meio dos operandos, que são os números. É a notação, ou forma de escrever, que nós humanos estamos mais acostumados a ver, mas não é a única notação e nem a melhor.</p>
<p>Quem tá acostumado com as boas e velhas calculadoras de engenharia da HP conhece a famosa notação RPN ou notação polonesa reversa onde começamos digitando 3, depois 2, depois asterisco. Daí ele multiplica o 3 pelo 2, que dá 6. Continuamos digitando o 1 e só depois o sinal de soma, que vai somar o 1 com o resultado parcial 6, e isso vai dar 7.</p>
<p>Notação polonesa reversa é o que chamamos de pósfixa. Portanto notação polonesa é a préfixa. E numa árvore de sintaxe escrevemos de forma préfixa. O importante é saber que o que você considera &ldquo;bom senso&rdquo;, nem sempre é, tem formas melhores. Vamos ver como fica. A árvore pra essa conta começa com um nó raiz que seria o sinal de mais. Começamos pelo operador, por isso préfixo. Daí divide em dois galhos, o nó da esquerda poderia ser o asterisco e o nó da direita o número 1. Embaixo do nó de asterisco, divide um nó a esquerda pra ser o 2 e outro a direita pra ser o 3.</p>
<p>Essa representação seria o equivalente a escrever direto em polonês como <code>+(*(2, 3), 1)</code>. Já viram isso antes? Vamos mudar o símbolo de &ldquo;+&rdquo; e chamar de <code>somar</code> e trocar asterisco por <code>multiplicar</code>, daí ficaria <code>somar(multiplicar(2, 3), 1)</code>. Ficou mais claro agora?</p>
<p>É assim que você programa. A maioria das linguagens de programação, no caso especial de contas aritmeticas, como soma ou multiplicação, deixa você escrever da forma infixa, mas internamente, depois do parsing, o compilador tá mudando pra forma préfixa na árvore, assim como todo o resto das suas funções. Isso tudo foi pra ilustrar rapidamente que a forma que você escreve o código não é a forma como o computador vai executar.</p>
<p>Vamos ver como isso funciona. Vamos fazer outro programinha idiota em Java, que pegue os 3 primeiros parâmetros, converta o texto em número, que vai ser os números 1, 2 e 3 que usamos no exemplo anterior. Então precisa chamar o método estático <code>parseInt</code> da classe <code>Integer</code>. Daí vou imprimir no console com <code>System.out.print</code> a soma de a com b vezes c, exatamente como no exemplo. Agora compilamos com <code>javac</code> e podemos chamar <code>java Calc 1 2 3</code> e deu 7, como deveria. Nenhuma surpresa aqui.</p>
<p>Mas o que de fato executou? Pra quem não sabe existe a ferramenta <code>javap</code> que vem no JDK de Java que serve pra desassemblar, pra mostrar o assembly de bytecodes de Java. Lembra no episódio do guia mais hardcore de introdução a computadores que eu mostrei uma parte do assembly da CPU 6502 que era usado no antigo Nintendinho? Mesma coisa, a ferramenta javac vai passar nosso código por um parser, fazer algumas mágicas que já vou explicar e cuspir instruções binárias de máquina, o que comumente se chama de bytecodes ou assembly. E o javap só mostra esses bytecodes na tela de forma que nós humanos conseguimos ler.</p>
<p>Parece complicado, mas pra esse exemplo simples não é. Esse trecho que vai até a linha 21 são as instruções pra pegar os argumentos que passamos e converter em inteiro, aquele <code>Integer.parseInt</code>. Percebam que a gente só escreveu uma linha de código pra cada um dos três parâmetros, mas pra cada um ele gerou pelo menos 4 instruções, isso sem contar esse <code>invokestatic</code> que chama seja lá o que o <code>parseInt</code> faça.</p>
<p>Novamente, o que você escreve na sua linguagem sempre vira bem mais instruções depois de compilado, e esse é o objetivo. Antigamente, até antes dos anos 90, era super comum escrever direto em assembly, em linguagem de máquina, porque era mais eficiente. Não havia nem memória, nem processamento suficiente pra existir um compilador inteligente como os de hoje.</p>
<p>Mas agora os compiladores evoluíram tanto que geram assembly muito melhor do que é possível fazer na mão. Além disso os programas que escrevemos hoje são muito maiores, seria impossível escrever tudo em assembly. Se alguém já se perguntou se seria mais eficiente escrever tudo em assembly, não, não é, tirando raríssimas exceções, nenhum programador hoje é melhor que um compilador.</p>
<p>De qualquer forma, a parte que importa são as linhas 24 a 28. Aqui esse bytecode <code>iload</code> vai empilhando na Stack os números que converteu. Daí chama o bytecode <code>imul</code> que, como dá pra ver pelo nome, é a multiplicação. Ele desempilha os últimos 2 números, multiplica e empilha de volta o resultado. Chega na instrução <code>iadd</code> que desempilha os últimos dois valores na Stack, soma e empilha o resultado. Lembram? Eu expliquei pilhas no episódio de Hello World como você nunca viu. Se não sabe como um sistema baseado em pilha funciona, assista esse episódio depois.</p>
<p>Os mais espertos aí assistindo talvez tenham notado, mas porque eu fiz essa forma complicada de ter que passar os números como parâmetros na linha de comando? Por que não deixei hardcoded direto no código o <code>1 + 2 * 3</code>? Vamos fazer isso agora. Deixa eu criar uma nova classe chamada <code>Calc2</code> e fazer direto <code>System.out.print(1 + 2 * 3)</code>.</p>
<p>Aliás, ignorem a nomenclatura de classes que estou usando, tá bem porco mesmo só pra ir mais rápido, mas obviamente não façam nomes como &ldquo;Calc2&rdquo; em projeto de verdade, né? Enfim, agora salvamos, compilamos de novo com <code>javac</code> e podemos executar com <code>java Calc2</code> e devolve 7 de novo. Se for como no exemplo anterior, agora deve ter cortado aquele monte de bytecodes de parseInt mas deve ter as mesmas instruções de iload pra empilhar e imul e iadd pra multiplicar e somar, né?</p>
<p>Vamos conferir. Fazemos <code>javap -c Calc2</code> e olha só, ué, cadê a multiplicação e adição? Não tem, em vez disso tem aqui esse bytecode <code>bipush</code> que quer dizer &ldquo;empilhe este número inteiro na Stack&rdquo;, no caso o número 7 que já é direto o resultado de <code>1 + 2 * 3</code>. Sacaram? Na hora de compilar o javac já viu: cara, essa conta vai dar 7 sempre, nunca vai mudar, então pra que vou refazer o cálculo toda vez? Deixa eu já pré-calcular e dar direto o resultado. Assim ele economizou 3 chamadas de <code>iload</code>, e as chamadas pra <code>imul</code> e <code>iadd</code>. Em vez disso ficou só uma chamada <code>bipush</code> pra empilhar direto o resultado final 7 e daí já pula pra chamada que vai imprimir o texto no console. O compilador literalmente reescreveu meu código.</p>
<p>Lembram daquele meu interpretador pra linguagem mais estúpida do mundo? Ele pega o código do meu programa, tokeniza e coloca os tokens num array, e faz um switch case, se achar o operador &ldquo;add&rdquo; executa uma soma, se achar um &ldquo;mul&rdquo; faz multiplicação. E olhando pra isso, vocês poderiam pensar, &ldquo;ah, isso faz sentido, pra que precisa de tanto trabalho pra converter os tokens numa árvore e depois da árvore converter em instruções de máquina em vez de direto já cuspir instruções?&rdquo; E esse é um dos motivos. Vamos filosofar um pouco.</p>
<p>Não importa que sintaxe de linguagens você ache mais bonita ou mais elegante ou mais produtiva. Não importa se você gosta de usar chaves pra delimitar funções como no Java ou se prefere usar identação como em Python. Não importa se prefere dividir tudo em várias pequenas funções de poucas linhas ou prefira o jeito go-horse de entuchar o máximo de linhas dentro de uma função. Não importa se gosta ou não de colocar comentários detalhados antes de cada função. Tudo isso é totalmente irrelevante pro computador.</p>
<p>Depois que seu código for parseado e compilado, o que vai sobrar são instruções de máquina. Muito do que você escreveu vai ser reescrito, o compilador vai pensar &ldquo;porque esse idiota deixou um cálculo hardcoded que sempre dá a mesma resposta? vou reescrever e deixar direto só a resposta, foda-se.&rdquo; Sim, o compilador vai jogar fora tudo que você escreveu e vai reescrever tudo do zero da forma que ele acha mais eficiente. É isso que o computador vai ver no final e executar.</p>
<p>Mas então, tudo que sempre me falaram de Código Limpo, boas práticas, serve pra que então? Se no final o computador tá cagando e andando, pra que eu tô perdendo tempo organizando todo meu código, escolhendo nomes fáceis de entender e tudo mais? Porque você não está e nem deve estar programando pro computador, você programa pra outras pessoas. Entenda essa verdade: o seu trabalho não é programar pro computador e sim pra que outras pessoas entendam. Incluindo você daqui alguns dias ou meses.</p>
<p>Se como eu você curte retrogames, já deve ter assistido videos explicando os truques que programadores faziam antigamente pra conseguir extrair o máximo de máquinas de processadores super fracos como o Z80 ou 6502, com quase nada de RAM, na faixa de 2 kilobytes. Pensa que o Super Mario World inteiro, com todos os gráficos, fases e lógica ocupa meio megabyte. Em meio megabyte hoje você não consegue fazer nada.</p>
<p>Qualquer página web simples precisa de múltiplos megabytes. E isso porque antigamente ciclos de CPU e RAM eram hiper caras. Pelo mesmo preço que se comprava um Nintendinho nos anos 80, hoje você compra um Playstation 5. Literalmente, no começo dos anos 80 um nintendinho custava quase 180 dólares, que se ajustar pra inflação dá quase 500 dólares, que é o preço de um PS5.</p>
<p>Por isso antigamente era hiper importante economizar ciclos e bytes ao máximo, mesmo que fosse ao custo de dificultar a vida dos programadores, porque o custo do hardware era muito maior que o custo de programadores. 10 kilobytes a mais de RAM que desperdiçasse em 1983 era 200 dólares a mais. Por isso escovar bits era a coisa mais importante. Mas o tempo passou e agora 200 dólares você compra mais de 16 giga de RAM. Por isso que desperdiçar 1 ou 2 gigas de RAM, por mais absurdo que pareça, não é mais grande coisa.</p>
<p>Por outro lado o que subiu foi a hora de programador. Se o programador tem que gastar o dia todo pra descobrir que diabos é esse número 7 e que a conta original era 1 + 2 * 3, que se estivesse explícito no código ia custar 2 segundos pra entender, faz muita diferença. Toda boa prática de código é feito pra economizar hora de programador e não hora da máquina. E mesmo se você trabalha sozinho, as boas práticas e código limpo continuam fazendo sentido, porque o você de amanhã, às 2 horas da manhã numa emergência, vai te agradecer por ter feito tudo organizado e fácil de ler.</p>
<p>Lógico, não estou dizendo que você não tem que saber programar direito sabendo como a máquina vai se comportar. Nem o melhor compilador do mundo vai melhorar seu código bosta que dá &ldquo;select *&rdquo; numa tabela gigante do banco só pra pegar uma linha, porque ele não tem como saber, durante a compilação, que pode ter uma tabela de um milhão de linhas sendo consumido e causando vazamento de memória. Ele vai fazer o melhor possível, mas não existe limites pra programador ruim dificultar a vida do compilador.</p>
<p>Mas a tecnologia de compiladores não serve só pra cuspir binários otimizados. Aquela árvore de sintaxe abstrata, a AST é útil pro programador. Sabe quando no VSCode você instala uma extension pra Javascript que roda o jslinter e ele marca no seu código onde tem problemas e possíveis bugs? Essa análise é feita em cima da AST. Ele não tá olhando o texto do seu código.</p>
<p>Por baixo dos panos ele passou pelo parser do V8, gerou a estrutura de árvore e tá processando os nós dessa árvore. Os nós podem conter metadados que apontam de volta qual linha do código que representa, e assim dá pra analisar e devolver a análise pro editor mostrar bonitinho pra você. Todo linter funciona assim. Um linter ou ferramentas de análise estática, tentam te orientar pra escrever código melhor e apontar coisas que seriam ambíguas pro compilador, daí ele pode te dizer o equivalente a &ldquo;tem certeza que é isso que você quer fazer?&rdquo;</p>
<p>O que mostrei até aqui é a fase de parsing. Essa idéia de criar uma gramática livre de contexto veio de ninguém menos que John Backus, criador do Fortran, provavelmente a primeira linguagem de alto nível de sucesso comercial, lá nos anos 60. E não só ele fez a linguagem como se inspirou nos trabalhos de pesquisadores como o grande Turing ou Noam Chomski, cujo foco era linguagens no termo mais genérico, tipo língua portuguesa ou inglês, mas os resultados poderiam ser aplicados em linguagens de computador.</p>
<p>O Backus inventou essa idéia de uma linguagem que descreve outra linguagem, ou seja, uma meta-linguagem, e usou pra definir uma nova linguagem chamada IAL, que viria a se tornar o ALGOL. Muita gente costuma pensar em C como o avô das linguagens, mais por causa dos blocos com chave e statements terminando com ponto e vírgula. Outros consideram o C muito novo pra ser avô e o verdadeiro avô seria o COBOL.</p>
<p>Mas eu penso diferente. No máximo, COBOL é mais avô de coisas como linguagens de stored procedures de banco de dados. A real linguagem avô do que chamamos de &ldquo;linguagens de uso genérico&rdquo;, anterior ao C, é o ALGOL. Antes do C ser inventado, o Ritchie e Kerninghan trabalharam no BCPL que poderíamos chamar de linguagem &ldquo;B&rdquo; e pra mim a linguagem &ldquo;A&rdquo; é o ALGOL.</p>
<p>Antes que alguém comente, sim, eu sei, não é exatamente assim. ALGOL foi um linguagem criada por comitê e, como tudo que é gerado por comitê, ficou uma linguagem bloated, complexa, gorda. Daí na universidade de Cambridge em Londres surgiu o CPL que é o Combined Programming Language, mas que obviamente muitos poderiam chamar de Cambridge Programming Language, pra ser o &ldquo;ALGOL com os pés no chão&rdquo;. Acabou não sendo muito bem aceita, a intenção foi melhor que a implementação. Povo de javascript já viu episódios desse tipo, como yarn querendo ser npm melhorado, ou Deno querendo ser Node melhorado.</p>
<p>Ainda em Cambridge tentaram simplificar o CPL com o Basic CPL ou BCPL do Martin Richards, pra ser &ldquo;só as coisas boas do CPL&rdquo;. Poucos anos depois o Ken Thompson chefiou o projeto de UNIX pras máquinas PDP/11 e fez um compilador pra uma versão reduzida do BCPL que chamou de linguagem B. Mas tanto BCPL quanto B eram limitadas. B era alto nivel demais e como eu disse antes, isso custava recursos escassos e caros de hardware. Por esses e outros problemas, surgiu Dennis Ritchie pra reescrever a linguagem que seria a sucessora do B pra ser mais &ldquo;próxima da máquina&rdquo; e daí surgiu o C.</p>
<p>Mas independente disso, está claro que as raízes das principais funcionalidades que reconhecemos hoje numa linguagem de uso geral surgiram com o Algol e por isso podemos considerar que ela é a verdadeira linguagem A. E a linguagem C é a versão que realmente vingou e serviu de inspiração pra linguagens mais modernas como C++, Objective-C, que inspiraram depois Java, Python, C# e todo o resto que vocês usam hoje.</p>
<p>De qualquer forma, o cientista da computação Peter Naur reconheceu o poder da idéia de metalinguagem do Backus e como já existia o termo &ldquo;Chomski Normal Form&rdquo;, ele decidiu que a notação do Backus deveria se chamar &ldquo;Backus Normal Form&rdquo; ou BNF. Mas tecnicamente não era uma forma normal, como apontou o lendário Donald Knuth e foi ele que sugeriu que BNF deveria ser pra &ldquo;Backus-Naur Form&rdquo;, e é assim que chamamos até hoje.</p>
<p>Mas como falei antes, poucos programadores hoje, eu incluso, estudam as definições matemáticas por trás das notações de linguagens, com o rigor que deveria, porque ferramentas como flex, bison, yacc simplificam tudo isso e a gente tem mais que se preocupar com o design da linguagem propriamente dita, que começa em escrever a tabela de lexemes e a gramática no formato que mostrei antes. Em particular, se tiver interesse em estudar mais sobre geradores de parsers, a que eu vejo que povo considera mais moderna é o ANTLR, que por acaso é feita em Java.</p>
<p>Um parser não serve só pra compilar linguagens de programação. Pense um arquivo de configuração em YAML ou um JSON. Ambos não deixam de ser linguagens, só que declarativas e não de programação. Ambas precisam de um parser e de fato, pra transformar um YAML ou JSON num objeto de javascript, ou um Hash em Python, precisa de um lexer e de um parser.</p>
<p>Sabe quando você carrega um arquivo texto de HTML num navegador e agora dá pra inspecionar usando as ferramentas de debug? Você que é de front-end sabe disso, ele se transforma no tal do DOM ou Document Object Model. E o que é o DOM? É uma árvore, é resultado de passar o HTML por um parser. E como o navegador desenha as coisas na tela? Navegando pode Node a Node do DOM. O DOM está pra HTML assim como AST está pra uma linguagem de programação. E da mesma forma que você pode modificar nós do DOM, o compilador manipula nós do AST pra melhorar seu código e gerar binários mais eficientes.</p>
<p>Tanto o que você chama de compilador quanto o que chama de interpretador começam com um lexer e um parser. Mas a história não acaba aqui. Agora vem a parte interessante da história. Lembra quando fizemos o disassembly do binário compilado de Java? Ele não era uma árvore né, era uma sequência de instruções, o que em Java se chama de bytecodes. Depois que o javac gerou o AST, checou que a sintaxe tá correta e as dependências estão corretas, daí navega nó a nó da árvore e cospe os bytecodes correspondentes pra gerar o arquivo &ldquo;.class&rdquo; no final.</p>
<p>Vamos recapitular, o que é um bytecode? É literalmente isso, um código de byte. Cada instrução como aquela aaload, iload, imul, iadd são mnemônicos pra um código binário. Por exemplo, aaload é o hexadecimal 32 ou o binário 0011 0010. Lembra, a máquina hardware, a CPU, só entende binário e internamente tem instruções que são representadas por certas sequências de bits.</p>
<p>Assista o episódio de Hello World ou outros videos da série &ldquo;Como Computadores Funcionam&rdquo; pra relembrar disso. Mas agora vem uma pergunta, que diabos de instrução é isso de aaload, iload, imul. Isso não é assembly de x64, que é o que roda em Intel ou AMD aí no seu PC ou notebook. Também não são instruções de ARM como os Mac M1 ou Qualcomm do seu Android. Que diabo de instruções são essas? E aqui vem a parte que confunde a diferença entre um compilador puro e um interpretador. Essas instruções são pra uma máquina virtual que não existe em forma de hardware.</p>
<p>Se existisse um chip de Java - e nos anos 90 a Sun até queria mesmo fazer um chip de verdade Java -, essas seriam as instruções. Mas esse chip não existe, o que existe é a JVM, que literalmente significa Java Virtual Machine. Todo programa Java roda numa máquina virtual. Por similaridade, todo programa Python roda numa máquina virtual Python. Todo programa Javascript roda numa máquina virtual Javascript, como o Google V8 ou Mozilla SpiderMonkey. Todo navegador web vem embutido com uma máquina virtual. E sim, máquina virtual como um Virtualbox ou VMWare, já pararam pra pensar nisso?</p>
<p>A gente tá acostumado a pensar em máquina virtual só como um Virtualbox ou Parallels ou erroneamente um Docker - que não é máquina virtual. Pensamos como programas que podemos instalar num Mac M1 pra conseguir rodar outros programas compilados como binários de Intel e interpretar em tempo real pra rodar numa máquina ARM. Tecnicamente, toda máquina virtual é um interpretador, no sentido que ele interpreta uma instrução de uma máquina pra instrução de outra máquina.</p>
<p>Pra quem é de Python, quando chamamos <code>python -c compileall .</code>, ele vai pegar todos os arquivos texto com extensão <code>.py</code> e transformar em <code>.pyc</code> ou <code>.pyo</code>. Pra isso o código texto vai passar por um lexer, por um parser e no final o AST resultante é transformado em instruções pra uma máquina virtual de Python. Aí ele serializa e salva esse AST num binário &ldquo;.pyc&rdquo;. O que chamamos de &ldquo;interpretador&rdquo; é um caso especial de máquina virtual. Alguns também chamam isso de &ldquo;runtime&rdquo;, mas são só nomes diferentes pra mesma coisa. Pra todos os efeitos e propósitos, seu programa Python tá rodando numa máquina virtual. O Javascript do seu navegador ou no Node.js também.</p>
<p>Dentre várias coisas que podem ser diferentes é que normalmente quando falamos de interpretadores é quando temos algumas facilidades de desenvolvimento, como um REPL que é acrônimo pra Read-Eval-Print-Loop, que é a linha de comando que abre quando você digita <code>python</code> ou <code>node</code> no terminal e pode sair digitando código que ele vai executando imediatamente, ou seja, ele lê, faz evaluation, imprime o resultado e volta pro loop e fica nisso até apertar <code>Ctrl+D</code> pra sair. Aliás, a linha de comando do seu terminal, seja Bash, ZSH ou Fish, é um interpretador também.</p>
<p>Sendo mais específico, um interpretador costuma manter o AST num formato que podemos modificar, depois dos arquivos do programa terem todos passados pelo parser. Toda vez que dentro do console de Python a gente define um novo método, estamos adicionando nós na árvore AST. É isso que define o que muitos chamam de &ldquo;linguagem dinâmica&rdquo;. Mesmo Java, apesar de mais chatinho, tem como modificar o AST que tá em memória. Eu posso mandar o classloader da JVM carregar outros arquivos &ldquo;.class&rdquo; que ainda não tinha carregado, ou usar a API de Reflection pra modificar o código em tempo real.</p>
<p>Claro, apesar de parecerem máquinas virtuais, interpretadores são diferentes de um VirtualBox. Um VirtualBox da vida de fato tenta ao máquina esconder tudo sobre a máquina host por baixo, pra fazer os programas rodando dentro acreditar que estão numa máquina de verdade. Quando um programa lá dentro pede pra fuçar o disco, o Virtualbox vai dar pra ele um disco virtual e não acesso ao disco de verdade.</p>
<p>Quando o programa pede pra conectar na internet, ele não vai ter acesso ao TCP da máquina de verdade, mas sim à uma rede virtual interna, como se fosse uma rede externa separada que faz ponte com a rede da máquina de verdade. E assim por diante. Já um interpretador não tenta esconder nada. Quando o programa de python pede acesso ao disco, o interpretador dá acesso ao disco de verdade. Quando o programa pede pra abrir um socket de rede, ele passa pela rede de verdade.</p>
<p>Então tecnicamente sim, Java é uma linguagem compilada porque no final gera um binário com instruções de máquina. Só que não são instruções de Intel ou ARM e sim pra JVM, uma máquina só virtual. Por outro lado, ela é interpretada porque a máquina pra qual ela foi compilada não existe e pra rodar depende de um interpretador que entende essas instruções e vai converter pras instruções da máquina host de verdade, como um Intel x64. O mesmo vale pra Python, Javascript, Ruby, PHP. E eu sei, já estou vendo você aí já indo ferozmente pros comentários pra discordar, porque eu ainda não mencionei JIT, segura a onda aí!</p>
<p>Já vou voltar no assunto de interpretadores e máquinas virtuais, mas o importante é saber que depois da fase de parsing, o objetivo é transformar a árvore abstrata de sintaxe em instruções de máquina, seja lá pra qual arquitetura de CPUs: uma de verdade como x64 ou arm64 ou uma virtual como a JVM. A história não acaba aqui.</p>
<p>Mesmo o compilador de C como GCC ou o clang de LLVM não sai da AST depois do parsing e já transforma direto em instrução de Intel. Não, acho que hoje em dia praticamente todos os compiladores trabalham com uma representação intermediária. O compilador de DotNet, por exemplo, chama isso de Intermediate Language ou IL. No mundo LLVM isso se chama Intermediate Representation ou IR. No mundo do GNU Compiler Collection ou GCC que todo mundo conhece, ele trabalha com RTL que é o Register Transfer Language. No mundo de shaders como pra rodar em Vulkan no SteamDeck, existe o padrão Standard Portable Intermediate Representation ou SPIR-V. E o que diabos é isso de linguagem intermediária?</p>
<p>Agora é a parte da mágica que eu falei antes que vou simplificar bastante, entenda que essa parte é absurdamente extensa na realidade. Vou usar o exemplo mais besta de todos. Lembra aquele exemplo da calculadora que escrevi em Java pra dar print em <code>1 + 2 * 3</code> e depois de compilar, fizemos o desassembly pra ver as instruções e só tinha um <code>bipush</code> direto do resultado 7? Pois é, o compilador foi inteligente em saber que não precisa traduzir instrução a instrução, a multiplicação e soma e, em vez disso, já pré-calculou e deixou direto o resultado, economizando várias instruções. Ou seja, ele otimizou o código e deixou tanto mais rápido quanto economizou ter que guardar os números 1, 2 e 3 na memória.</p>
<p>Essa é a otimização mais trivial e besta de todas, mas o compilador tem a capacidade de fazer centenas de outras otimizações. Por exemplo, digamos que seu código tenha trechos que nunca usa. Dependendo da linguagem e agressividade da otimização, ele pode até escolher jogar fora esse trecho e nem traduzir. Sim, o compilador pode escolher jogar seu código fora. Ele pode simplificar chamadas. Se a ordem dos fatores não influir no resultado, pode mudar a ordem do seu código pra CPU processar de forma mais eficiente.</p>
<p>Essa fase de otimização pode passar por todo o código mais de uma vez, é o que se chama de &ldquo;passes&rdquo;. Isso pra conseguir fazer coisas como achar constantes duplicadas e fazer ambas apontarem pro mesmo lugar. Eliminação de código morto, ou seja, trechos que nunca são executados e podem ser eliminados. Tail Call elimination, pra remover chamadas recursivas que podem ser otimizadas. E muito, muito mais. Isso é o que o LLVM chama de Transformation Passes.</p>
<p>Entre aspas é como se você tivesse um Linus Torvalds e um John Carmack juntos, reescrevendo seu código da maneira mais otimizada possível. Na prática essa etapa é o real trabalho de um compilador e onde se gasta mais tempo e mais recursos, porque tem um gerador de código varrendo tudo que você escreveu, exaustivamente, várias vezes, e reescrevendo da melhor forma que se conhece sem quebrar, nem gerar bugs.</p>
<p>Agora, imagine o seguinte: tá vendo essa lista de transformações e otimizações? Imagine se toda vez que inventa uma nova linguagem, se precisasse reescrever todas essas estratégias tudo de novo pra linguagem nova. Seria um trabalho absurdo, que ia gerar dezenas de bugs, e as otimizações seriam ruins porque criar essas rotinas levou anos e milhares de horas homens pra aperfeiçoar até ficarem perfeitas. Por isso que o comum é que toda nova linguagem, no final do parsing, seja convertida numa linguagem intermediária, que é sempre igual. E essa linguagem costuma ser tipo um assembly pra uma máquina virtual, que não precisa se preocupar nesse ponto com as limitações de design e legado de uma CPU de verdade com um x64.</p>
<p>Daí todas essas estratégias de otimização são executadas nessa linguagem interdiária. Um compilador como GCC ou derivados de LLVM na verdade são programas grandões que podemos dividir em 3 grandes funções diferentes. O primeiro é o front-end, que tem a responsabilidade de pegar linguagens como C, C++, Go, Rust, Swift e fazer o lexing, o parsing, gerar a árvore de sintaxe e dela cuspir as instruções de máquina virtual intermediária, sem quase nenhuma otimização. Essas instruções intermediárias são o RTL, no caso de GCC, ou IR no caso de LLVM, ou IL no caso de DotNet. O clang de LLVM seria o front-end de C pra LLVM, o conjunto das definições de lexer e gramática do parser.</p>
<p>Agora o 2o programa pega esse IR não otimizado e gasta um tempão lendo, relendo e reescrevendo, e a saída vai ser outro IR agora totalmente otimizado. Esse programa do meio poderia ser chamado de &ldquo;middle-end&rdquo;. E relembrando, cada uma das otimizações ou transformation passes que ele faz poderia ser tema de um paper de Ph.D. Não são coisas triviais como o exemplo que eu mostrei. E finalmente temos o 3o programa, que chamamos de &ldquo;back-end&rdquo;, que vai pegar o IR otimizado e traduzir finalmente pras instruções da máquina de verdade, ou seja, converter de instruções assembly de IR pra instruções assembly de x64 ou arm64 da vida. E esse é o binário final que pode ser executado.</p>
<p>Toda vez que que eu quiser inventar uma nova linguagem é só fazer as partes de lexer e parser, fazer só a parte que é o front-end dessa nova linguagem. É pra isso que servem ferramentas como o ANTLR que eu mencionei antes. É isso que linguagens como Rust fizeram quando construíram o compilador em cima de LLVM. Ele pode deixar um compilador que já existe otimizar o que puder. Num primeiro momento, o criador de uma nova linguagem não precisa refazer tudo do zero, pode reaproveitar o que já existe, porque uma vez tendo IR, o resto é igual. A preocupação inicial dele é só fazer um parser que converte sua linguagem em IR, como um transpiler que converte TypeScript em Javascript, por exemplo.</p>
<p>Quando um designer de chips como uma Apple inventa um novo como os tais M1, ele só precisa fazer a parte back-end, a parte que pega o IR já otimizado e converte em instruções de máquina. Amanhã a Qualcomm lança um novo chip SnapDragon ARM, ele só precisa atualizar o back-end do compilador, todo o resto continua igual. A AMD lança a nova série 6000 do Ryzen, só precisa atualizar o back-end dos principais compiladores se teve alguma mudança nas suas instruções. Entenderam?</p>
<p>Eu expliquei isso no video de Apple e GPL, mas além obviamente do hardware, qual é o grande segredo da Apple em conseguir migrar tudo pra M1 tão rápido? É eles terem investido quase 2 décadas melhorando suas tecnologias de compiladores em cima de LLVM. Com isso conseguiram migrar de PowerPC pra Intel nos anos 2000 e com isso migraram de Intel pra ARM muito rápido. E tudo que só o compilador não consegue resolver, eles tem como desenhar no chip pra compensar. E com isso temos as performances absurdas que vocês assistem nos diversos reviews de como programas originalmente compilados pra Intel rodam liso em ARM sem recompilar, só sendo interpretados em tempo real.</p>
<p>Se você é um cientista da computação e tem vontade de criar uma nova linguagem, provavelmente vai começar fazendo um front-end pra LLVM. Assim saíram linguagens como Rust da Mozilla, Julia, Crystal e outros. Pouquíssimas empresas tem capacidade pra fazer um compilador do zero com todos os tipos possíveis de otimização. Java tem otimizador próprio desde a era da Sun e depois passou pra Oracle e tem dezenas de empresas que investem em pesquisa pra continuar melhorando. O DotNet tem a Microsoft, que também investe pesado em pesquisa. E no meio disso tem o Javascript e o V8 do Google.</p>
<p>Agora, qual a diferença de C, Java e Javascript? No final do dia quer dizer que C é compilado porque gera binário nativo de máquina, mas Java e Javascript são interpretados? Não, no final do dia, todos geram binário nativo de máquina usando estratégias diferentes, pra resolver problemas diferentes. A estratégia de C se chama ahead of time compiling ou AOT. E no Java e Javascript eles usam Just in Time compiler, o famoso JIT. E mesmo JIT tem diversas estratégias diferentes.</p>
<p>Vou fazer outro exemplinho bem besta só pra vocês terem uma imagem na cabeça. Vamos voltar pro exemplo em Java de fazer um cálculo. Quem lembra como calcula a circunferência de um círculo? Lembra? Duas vezes Pi vezes Raio? 2 pi r. Então vamos fazer uma nova classe, método estático main, e System.out.print, 2 vezes 3.141592 etc que é PI mas vou colocar poucos dígitos pro exemplo, e vezes 10 que é o raio que quero usar. Salvamos, saímos, compilamos com <code>javac</code>, rodamos e voilá, 62.83 bla bla é a circunferência pra um raio 10.</p>
<p>Se fizermos disassembly com <code>javap</code> olha aqui embaixo essa instrução <code>ldc2_w</code> que puxa uma constante do pool de constantes e manda pra stack, e no comentário gerado temos que essa constante é um double 62.83 bla bla que é o valor pré-calculado. Ele fez a mesma otimização que no caso do <code>1 + 2 * 3</code>. Top, é o esperado. Mas como eu disse antes, olha como essa continha no código é feio, mesmo sendo simples. Quem curte código limpo tem gatilho só de ver isso. Dá pra melhorar isso.</p>
<p>Vamos escrever uma nova classe. Eu poderia começar definindo uma constante double chamada PI e escrever ali, 3.141592. Mas isso seria idiota porque obviamente Java já tem uma constante estática double de PI, que tá na classe Math. Ah, mas como eu ia adivinhar? Sempre pense: coisas triviais assim, as chances são que já existe. Na dúvida procurar no Google, &ldquo;Java constant PI&rdquo; e olha só, nos primeiros links já vem Math.PI. Quando você não acha as coisas não é que não existem ou são difíceis de achar, você que não procurou direito.</p>
<p>Enfim, continuamos fazendo uma função estática que retorna double chamada <code>circunferencia</code> que recebe como argumento uma variável double chamada <code>raio</code>. Dentro calculamos o 2 vezes <code>Math.PI</code> vezes a variável raio. E agora sim, no método <code>main</code> vamos dar <code>System.out.print</code>, como antes, mas o argumento vai ser o resultado do método <code>circunferencia</code> passando o valor 10. Pronto, salvamos, saímos, compilamos com <code>javac</code> e executamos e tá aí, 62.83 bla bla como tinha dada na outra versão.</p>
<p>Compara a versão antiga com a nova. Ficou maior, mas a intenção do código tá muito mais clara. Se daqui 1 mês eu for editar esse código, dá pra saber o que ele faz imediatamente. E claro, dava pra ter feito bem mais bonito que isso, fazer uma classe chamada <code>Circulo</code> ou um módulo chamado <code>Geometria</code> ou qualquer coisa assim, mas hoje não é design de orientação a objetos, então não impliquem muito com isso hoje.</p>
<p>Mas vamos ver como fica fazendo disassembly com <code>javap</code> e olha que estranho, tem a instrução <code>ldc2_w</code> que puxa a contante 10 que é o raio que eu defini e puxa pra stack, daí invoca a função <code>circunferencia</code> e dá jump aqui pra cima. Daí faz outro <code>ldc2_w</code> pra puxar a constante de PI pra stack também, na verdade a constante de <code>2 * Pi</code>, veja o comentário que ele gerou mostrando que é o dobro de PI, então ele otimizou isso. Mas aí chama a instrução <code>dmul</code> que é multiplicação de double e desempilha os últimos dois valores que colocamos na stack pra calcular, retorna e finalmente dá <code>invokevirtual</code> no printf pra imprimir na tela.</p>
<p>Mas <code>2 * pi * 10</code> é tudo constante, porque ele não otimizou o suficiente pra arrancar fora essa função <code>circunferencia</code>, ela não seria desnecessária? Eu não disse que o compilador reescreve tudo pra ficar mais eficiente? Não dava pra ter pré-calculado como antes e só guardar a constante com o resultado final? Então, aqui que a coisa começa a complicar. Toda função que criamos no Java se torna uma interface. Se o compilador quisesse ser agressivo ele poderia arrancar tudo fora e deixar só a constante, porém a gente não sabe se ninguém mesmo vai usar essa função. Lembra? Java também é uma linguagem interpretada, dinâmica, cujo código pode ser alterado dentro da JVM.</p>
<p>Se pensar isoladamente só nessa classe, não teria problema, mas classes Java não vivem no vácuo, elas foram pensadas pra serem reusadas. Por exemplo, no mesmo diretório posso criar um novo arquivo e escrever uma nova classe assim, fazendo de novo um <code>System.out.print</code>, chamando <code>Calc3.circunferencia</code> direto assim mesmo e passando 20 como raio. Se eu sair e compilar só esse arquivo calc5.java, posso executar <code>java Calc5</code> e tudo funciona. Note que nem na hora de compilar e nem na hora de executar eu mencionei o Calc3, porque ele tá no mesmo diretório, então quando a JVM carrega a classe Calc5 automaticamente faz o Class Loader carregar o Calc3 junto. Mas ele não precisou recompilar o Calc3 de novo.</p>
<p>E se eu apagar o Calc3.class e tentar executar o Calc5? Agora vai dar a erro de <code>NoClassDefFound</code>. Faz sentido né? Java foi feito de tal maneira que eu não preciso recompilar tudo do zero toda vez que alguma coisa muda. Ele compila cada classe individualmente, e pra garantir que outras classes não quebrem, pelo menos mantém a interface entre eles estável, o que significa que não pode sair otimizando demais e eliminando métodos, mesmo se naquela classe ela não é usada. E mesmo se eu tivesse declarado como <code>private</code>, ainda assim não daria pra apagar o método, porque Java tem sistema de Reflection que permite manipular métodos privados. Portanto, qualquer otimização que quebre a interface não pode ser feita.</p>
<p>Antes de continuar, agora precisamos entender rapidamente o que é uma interface. No mundo binário, não existe o conceito de abstração, tudo são instruções e endereços. Quando precisa chamar uma função, a máquina na verdade vai guardar o endereço que estamos executando  na pilha, vai fazer o jump, vai executar o que precisa, e no final vai retornar pro endereço que tinha guardado, pra continuar de onde parou. Eu expliquei isso no video do Hello World.</p>
<p>Agora, pra isso funcionar, quando eu compilei a classe Calc3, o compilador precisaria ter anotado que a função <code>circunferencia</code> vai estar disponível num endereço X, faz de conta 9000 0000. Daí quando eu compilei a classe Calc5 que chama essa função, nesse ponto o compilador precisaria saber que precisa escrever um jump pro tal endereço 9000 0000. Mas lembram que eu não passei o Calc3 como parâmetro nem nada? Como que o Java sabe pra qual endereço tem que dar jump?</p>
<p>Aqui estou especulando, mas imagino que quando a JVM carrega essas classes, nesse momento que ela vai preenchendo uma tabela interna que diz, &ldquo;ok, carregar classe Calc3, ela tem um método circunferencia, vou colocar ela no endereço que tem vago aqui, 9000 0000&rdquo;. Depois carrega a Calc5 e quando executa a função <code>main</code> dela e de lá pede pra chamar a função <code>circunferencia</code>, daí a JVM intercepta essa chamada e consulta sua tabelinha &ldquo;hmmmm, essa função é a que eu coloquei no endereço 9000 000, pula pra lá&rdquo;. Portanto os endereços não estão nas classes, elas são atribuídas na hora que a JVM carrega elas.</p>
<p>Vamos abrir o disassembly de novo, começa executando lá da linha 0 e vai indo. Uma hora esbarra nessa linha 6, que tem um <code>invokestatic</code> passando um índice 15. Ele olha lá na tabela interna dele e vê &ldquo;ah, número 15 é o endereço 9000 0000&rdquo; e aí dá o jump pro lugar certo. Eu simplifiquei mas justamente porque os endereços finais são definidos quando carrega o módulo e não quando compila, dá pra compilar as classes independentes uma da outra, como eu fiz aqui, compilando individualmente os dois arquivos, porque a JVM é quem vai gerenciar os endereços em tempo de execução.</p>
<p>Porém, linguagens como C, C++, Swift, Go, Rust, Crystal, e outras não tem um interpretador ou máquina virtual pra controlar isso, portanto cada parte de cada módulo sendo compilado precisa já saber o endereço final das coisas, senão na hora de executar não ia achar nada. Como que faz?</p>
<p>E aqui vem outro conceito de compilador que vou simplificar ao máximo. Pensa, se eu tivesse um projeto com mil arquivos, onde uma função num arquivo pode chamar funções em outros arquivos, seria uma zona pra compilar. Toda vez que eu mudo uma linha num arquivo, precisaria recompilar todos os mil arquivos pra garantir que os endereços de todo mundo estão corretos e eu não apaguei uma função ou deu conflito de endereços ou duplicou endereços sem querer. Então na realidade, tanto o GCC quanto um clang de LLVM quanto um rustc de Rust, vão quebrar a parte final da compilação em duas etapas.</p>
<p>Se você é de Linux e já instalou um pacote que compila a partir do código fonte, já deve ter esbarrado num <code>Makefile</code>, que nada mais é do que um script que automatiza a função de checar dependências, se tudo que precisa pra compilar tá instalado. Pra você que é de Javascript é o equivalente a um package.json. Pra você que é de Python é tipo um requirements.txt. Ele chama o compilador pra todos os arquivos do projeto. O que costuma aparecer na tela enquanto compila? Um monte de arquivos <code>.o</code> sendo gerados.</p>
<p>Esses &ldquo;.o&rdquo; são objetos, simplificando, eles são os binários compilados a partir de código fonte de C de cada arquivo. Vamos abrir o hello.c lá do começo. Tão vendo esse <code>include &lt;stdio&gt;</code> na primeira linha, isso indica pro compilador procurar por um arquivo de cabeçalho chamado <code>stdio.h</code> que tem que estar no Path do sistema. Nesse arquivo vai constar a interface da função <code>printf</code> que usamos lá embaixo. Se olharmos nesse arquivo tá dizendo que o <code>printf</code> é uma função que retorna inteiro e recebe como parâmetro um ponteiro constante de char, basicamente um array de chars, e tem outros argumentos opcionais.</p>
<p>Sem entrar muito em detalhes sobre o C, ter essa interface permite o compilador saber se quando estamos usando a função <code>printf</code>, se tá da forma correta. Porém, no momento da compilação do arquivo <code>hello.c</code> pro objeto binário <code>hello</code> ele ainda não sabe qual é o endereço onde encontrar a função, porque ela tá numa biblioteca externa, a libc, que é compartilhada com outros programas no mesmo sistema que dependem dela. Praticamente tudo numa distro Linux.</p>
<p>No ponto onde chama o <code>printf</code> o compilador vai tipo deixar um post-it dizendo &ldquo;confirmar depois onde fica esse endereço&rdquo;. Se eu dependesse de outras bibliotecas externas, ia ficar deixando vários post-its pra confirmar depois. Pra ilustrar, vamos fazer o disassembly do objeto hello usando o <code>objdump -d</code>. Olhar assembly sempre é meio intimidante, mas não tem problema, só focar no pedaço que interessa que é a função <code>main</code> que começa aqui a partir do endereço 1139 em hexa. O <code>printf</code> é uma função então estamos procurando um <code>call</code>.</p>
<p>E no endereço 1147 temos um suspeito, um <code>call</code>, mas pra um endereço aqui perto, em 1030. Vamos ver o que tem lá. E olha lá, tem um jump pra esse outro endereço e um comentário que é pra função <code>puts</code> da glibc. Se não estou enganado, quando se chama o printf com o parâmetro opcional nulo, ele muda pra puts. Mas nessa linha, na primeira fase do compilador, quando ele ainda não sabe os endereços, vai ter algo como um espaço em branco pro endereço, um post-it pra segunda fase confirmar o endereço.</p>
<p>Essa segunda fase é o trabalho de um linker, no caso do GNU LD. Até agora eu não precisei chamar o linker manualmente porque o GCC faz isso sozinho pra gente. Mas se eu quiser manipular vários objetos já compilados e transformar numa biblioteca compartilhada, como um <code>.so</code> de Linux ou <code>.dll</code> de Windows, posso fazer isso com o comando <code>ld</code>.</p>
<p>Inclusive, o linker pode fazer otimizações em tempo de linking. Apesar do nome ser linker que é linkar ou grudar os pedaços, ele pode modificar mais do binário. É ele que sabe se é possível por exemplo fazer inlining. Lembra o exemplo besta de Java de circunferência? Se fosse em C, como ninguém mais chama aquela função, ele poderia simplesmente copiar o conteúdo da função e colar no lugar onde a função é chamada. Isso evita no mínimo uma instrução de jump e outra de return. Num programa maior tem muito mais ganhos.</p>
<p>No final, o compilador de C consegue analisar seu código e realizar um monte de otimizações e reescrever de uma forma otimizada, mas isso arquivo a arquivo independentemente, daí o linker reavalia  os binários gerados, liga os endereços de cada um onde precisa e ainda otimiza mais, o que diminui mais ainda o tamanho do binário, economiza mais memória e processamento na hora de executar, e por isso programas em C e similares como Rust são tão rápidos.</p>
<p>Linguagens como Java, Javascript, Python, e outros não tem o equivalente de um linker quando compilam do código fonte pra bytecode intermediário. Uma das razões disso é aumentar a reusabilidade desses binários, o que também torna a fase de compilação menos demorada que um C pra um projeto do mesmo tamanho. Porém o binário vai ser menos otimizado, tecnicamente mais lento do que o equivalente gerado por C. Então é isso? C sempre vai ser mais performático que qualquer coisa? Não é bem assim. E agora voltamos pra Just In Time Compiler, os JITs.</p>
<p>Quando você carrega um projeto em Java, ele inicia a máquina virtual JVM, daí o class loader começa carregando os arquivos binários <code>.class</code> do seu projeto e criando tipo uma tabelona interna que diz em quais endereços internos na memória estão quais classes e quais funções, de forma que quando uma classe pede pra executar uma função de outra classe, a JVM vai intermediar e arbitrar mandando executar a função correta, devolvendo o retorno e tudo mais.</p>
<p>Um interpretador ou máquina virtual substitui a necessidade de um linker estático, porque ele cuida disso dinamicamente. Então você pode imaginar que o C, que não tem esse intermediário já precisa saber de antemão o endereço de tudo. Se de fato toda vez a JVM precisar ficar checando os endereços nessa tabela virtual pra cada chamada de função, seria tudo lento mesmo. Mas claro que no mínimo ela faz cache disso pra ir mais rápido. Mas ela pode fazer mais. Ela analisa os códigos mais executados, por exemplo baseado em quais caches são mais pedidos, os &ldquo;pontos mais quentes&rdquo; do programa, literalmente o HotSpot, que é o nome da máquina virtual de Java quando ele ganhou capacidades de just in time compiling, ou JIT.</p>
<p>Se meu programa chamasse aquela função circunferencia diversas vezes sempre com o mesmo raio, em vez de toda vez executar o cálculo, o JIT vai ser esperto e cachear o valor final. Muitas das otimizações que eu falei que o linker faz no C em tempo de compilação, o JIT vai fazer durante a execução. Por isso que em Java você já tenha ouvido falar de &ldquo;tempo de aquecimento&rdquo;, ou warm up. Logo que inicia um servidor Java, ele vai ter uma performance X que pode até parecer baixa, mas deixe rodando um tempo, e o mesmo código começa a executar mais rápido. O JIT é um otimizador em tempo real, aplicando mais e mais otimizações nos códigos mais acessados.</p>
<p>Esse JIT, no final, tá traduzindo o bytecode do Java em instruções nativas de máquina de verdade mais otimizadas. É literalmente um compilador e mais ou menos um linkeditor que só trabalha em cima de código que realmente é necessário. Se seu programa for grande e tem partes que raramente são acessados ou ninguém nunca acessa, eles continuam lá sem serem otimizados. E o JIT vai fazendo cache do que vai otimizado, assim da próxima vez que alguém chamar essa parte, ele só executa o binário nativo de novo. E nesse ponto, esse trecho do código pode estar quase tão rápido como se tivesse sido escrito em C. Claro, não é só isso que determina a performance final, mas o JIT diminui bastante a distância entre traduzir bytecode Java de forma interpretada e um binário nativo compilado de C.</p>
<p>A mesma coisa acontece com Javascript no navegador. Navegadores baseados em Chromium, como o próprio Chrome, Microsoft Edge, Opera, Brave, todos acabam usando a máquina virtual Google V8 pra executar Javascript. V8 na verdade é o nome de um conjunto de tecnologias de compilador e máquina virtual, assim como Java ou Python. Internamente ele tem um lexer, um parser, que vai cuspir um AST e que por sua vez vai gerar uma representação binária intermediária em bytecodes específicos da máquina virtual do V8.</p>
<p>Sim, o que roda no V8 não é javascript, são esses bytecodes. Repetindo, elas são como instruções de um x64 de verdade, mas pra uma máquina javascript que não existe. Nesse arquivo bytecode.h tem declarado todos os bytecodes. Vou deixar nas descrições abaixo pra quem tiver curiosidade.</p>
<p>Uma ver convertido em bytecodes, o interpretador do V8, que se chama Igniter, começa a traduzir instrução a instrução. Não vai ser lento, mas também não vai ser a melhor performance. Assim como na JVM, ele vai ficar analisando a execução e perceber quais partes precisa otimizar. Digamos que sua aplicação web baixou trocentas bibliotecas, por exemplo o arquivo inteiro do jQuery. Mas na real você só precisa da função <code>$()</code> pra achar um elemento da página e a função <code>fadeToggle</code> pra criar uma transição animada de fade desse elemento. Todas as outras funções você não usa.</p>
<p>Se o V8 fosse burro e tentasse compilar tudo que sua página baixa, ele ia gastar um tempão pra otimizar a biblioteca do jQuery inteiro, quando na verdade sua página só usa meia dúzia dessas funções. Mas à medida que o usuário navega pelo site, o V8 acha os hotspots e agora é hora do Just in Time Compiler, do JIT, entrar em ação, no caso o Google chama o JIT do V8 de TurboFan. É ele quem vai gastar memória e processamento pra otimizar as partes mais usadas e gerar binário nativo de máquina otimizado, parecido com o que o Java faz, e jogar num cache em memória ou até em disco. E por isso programas em Javascript costumam consumir muita memória.</p>
<p>Por isso que tanto Java quanto Javascript conseguem atingir velocidades muito boas, às vezes próximas de C mesmo sem terem sido agressivamente otimizadas por um compilador antes. Mas por que se dar a tanto trabalho e não simplesmente compilar tudo antes como o C faz? E isso não tem uma resposta única, mas se eu fosse resumir é o que define a diferença entre linguagens estáticas e dinâmicas.</p>
<p>A vantagem de C é que os compiladores amadureceram muito nos últimos 40 anos e o binário nativo final que geram tendem não só a serem os mais rápidos em geral, mas também a ocupar bem menos espaço em disco e a consumir menos memória quando carregam. Por isso que a kernel do Linux, drivers e coisas assim tendem a serem feitos com C, porque eu quero o mais otimizado possível e que escale bem, pra rodar tanto num servidor de datacenter com centenas de núcleos de processador e terabytes de RAM quanto num mero Raspberry Pi com quase nada de recursos.</p>
<p>Uma vez que o binário tá compilado estaticamente, é tudo fixo e imutável, você não tem como mudar o comportamento dele. Sim, se tiver muita paciência pra explorar coisas como injeção de código direto na memória do processo ou fazer patches de binários, explorar exploits de segurança, aí você consegue mudar alguma coisa. Mas isso é na gambiarra. É o que a comunidade de mods de games faz também. Eles descobrem em qual endereço da memória que fica o valor de vidas e eu injeto outro valor lá. Já mostrei isso no video do Super Mario.</p>
<p>Por outro lado pense no seu navegador web. A qualquer momento eu posso inspecionar um elemento de qualquer página e sair manipulando. Eu mesmo, quando faço capturas de tela pra mostrar a imagem de um site nos videos, se tem uma propaganda muito grande, abro o inspetor, seleciono o elemento de propaganda e apago. E essa mudança se reflete na página imediatamente. Isso só é possível porque o navegador mantém a estrutura de dados que representa essa página em memória de forma acessível via Javascript. E como o código javascript não é estático, não é fixo, porque tem um interpretador que aceita código novo sem precisar reiniciar, dá pra fazer essas coisas.</p>
<p>A grande vantagem de linguagens dinâmicas é permitir esse tipo de flexibilidade. Se isso é bom ou ruim, depende do seu uso. Tem gente que acha que isso não deveria ser permitido, que uma vez compilado, deveria ser tudo fechado. Tem gente que acha que precisa ser tudo aberto, independente se vai ficar mais lento ou consumir mais memória. Como eu sempre digo, em programação a gente troca uma coisa por outra, sempre. Não existe resposta definitiva, tudo depende de pra que vai usar. E no caso de um navegador web acho que todo mundo vai concordar que ser aberto e dinâmico é muito mais prático e é por isso que a Web em si fez tanto sucesso.</p>
<p>Além disso linguagens dinâmicas como Javascript, Ruby, Python, Elixir, Java, C#, possuem capacidades muito potentes de metaprogramação, onde você pode escrever código que altera que está em execução. Praticamente toda linguagem com um interpretador possibilita isso. Java tem Reflection, Javascript ES6 tem APIs como Reflect ou Proxy, Python tem a função <code>type</code>, Ruby tem coisas como Refinements. E a vantagem de um interpretador é que podemos injetar novos bytecodes sem reiniciar o programa. Como a maioria dos interpretadores possuem algum tipo de JIT, esse novo código também pode gerar binário nativo otimizado.</p>
<p>Pra não complicar vou pular os detalhes sobre metaprogramação e a diferença com pré-processamento estático, templates como em C++, ou macros como em Rust. Estudem se tiver interesse, mas a conclusão de tudo isso é dizer que linguagens estáticas costumam ter um compilador que gera binários estáticos, cujo bytecode não é mais modificado facilmente. Linguagens dinâmicas costumam ter um interpretados justamente pra ter essas propriedades de dinamicamente modificar o bytecode e alterar o comportamento do programa em tempo real de execução. O custo disso é serem mais lentos, porque não puderam contar com a otimização agressiva de um compilador e linkeditor.</p>
<p>Mas hoje em dia essa diferença diminuiu drasticamente, porque quase todo interpretador vem com um compilador embutido que roda em tempo real, um Just in time compiler, ou JIT. No fim do dia, ambos acabam gerando binário nativo otimizado, um gera tudo antes, o outro gera enquanto roda. Um usa menos recursos, porque fez toda a compilação e otimização antes, e o outro usa mais recursos, porque vai continuamento compilando à medida que o programa é executado.</p>
<p>Como o video ficou super longo já, no próximo video vou dar uma pincelada em cada linguagem individualmente, mas antes vocês precisavam entender a diferença entre compiladores e interpretadores. Ser interpretado não implica ser sempre &ldquo;lento&rdquo;, só às vezes. Um exemplo, quem já ouviu falar de Elixir e Erlang provavelmente reconhece sua reputação de ser altamente escalável, tanto que é com ele que é feito o motor de comunicação por trás do Whatsapp, que é gigantesco. Mas nem todo mundo entende que Erlang, assim como Java, é uma linguagem interpretada, que roda numa máquina virtual, que é um tipo de interpretador, e que tem um Just in Time Compiler, ou JIT, pra acelerar as coisas.</p>
<p>Pela popularidade do Javascript, muita gente já sabe que as performances absurdas que ela consegue ter, o suficiente pra portar um game de verdade e rodar dentro do navegador é em grande parte graças ao JIT. Mas tem mais detalhes disso. E muito antes de Typescript e todas as linguagens transpiladas pra Javascript, um dos primeiros transpilers apareceu no mundo PHP com o HipHop do Facebook, que transpilava PHP pra C++. Hoje em dia eles pararam esse projeto e estão investindo mais na máquina virtual HHVM que é compatível até certo ponto com PHP 7. Python, consegue gravar os bytecodes da fase de parsing como arquivos <code>.pyc</code>, que são instruções pra sua própria máquina virtual. Ruby também gera bytecodes internamente. Enfim, tem bem mais coisas que eu vou guardar pro próximo video.</p>
<p>A intenção do video era só educar vocês pra não ficarem com cara de bobos argumentando coisas como &ldquo;ah, sua linguagem é interpretada, por isso nunca vai ser tão rápida quanto uma compilada&rdquo;, ou &ldquo;ah Java é melhor que Javascript porque ela é compilada&rdquo;. A grande verdade é que tecnologias de compiladores e máquinas virtuais evoluiu bastante nos últimos 20 anos e essas noções erradas que eram até corretas nos anos 90, já não é mais tão simples assim. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e não deixem de compartilhar o video com seus amigos. A gente se vê, até mais.</p>
]]></content:encoded><category>linguagens-de-programacao</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #103 - Todos os Sistemas de Arquivos: de FAT a ZFS</title><link>https://akitaonrails.github.io/2021/08/03/akitando-103-todos-os-sistemas-de-arquivos-de-fat-a-zfs/</link><guid isPermaLink="true">https://akitaonrails.github.io/2021/08/03/akitando-103-todos-os-sistemas-de-arquivos-de-fat-a-zfs/</guid><pubDate>Tue, 03 Aug 2021 11:23:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/71o6Tyh2MUk"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Quais são todos os filesystems de todos os sistemas operacionais? Por que todos falam que ZFS é o melhor? E quais as alternativas mais modernas? Vamos ver tudo isso hoje!&lt;/p&gt;
&lt;h3&gt;Conteúdo&lt;span class="hx:absolute hx:-mt-20" id="conteúdo"&gt;&lt;/span&gt;
&lt;a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;00:32 - INode, DEntry&lt;/li&gt;
&lt;li&gt;02:35 - VFS&lt;/li&gt;
&lt;li&gt;03:18 - Journaling&lt;/li&gt;
&lt;li&gt;05:54 - Filesystems com Journal&lt;/li&gt;
&lt;li&gt;06:23 - Filesystems da Apple&lt;/li&gt;
&lt;li&gt;07:59 - Filesystems de Linux&lt;/li&gt;
&lt;li&gt;11:21 - Resumo&lt;/li&gt;
&lt;li&gt;13:26 - ZFS&lt;/li&gt;
&lt;li&gt;14:36 - checksums&lt;/li&gt;
&lt;li&gt;16:28 - OpenZFS&lt;/li&gt;
&lt;li&gt;18:11 - snapshots - Copy on Write&lt;/li&gt;
&lt;li&gt;22:25 - de-duplication&lt;/li&gt;
&lt;li&gt;23:46 - CoW vs Journal&lt;/li&gt;
&lt;li&gt;24:49 - APFS&lt;/li&gt;
&lt;li&gt;26:01 - compressão&lt;/li&gt;
&lt;li&gt;26:32 - encriptação&lt;/li&gt;
&lt;li&gt;29:04 - BTRFS&lt;/li&gt;
&lt;li&gt;32:12 - o filesystem universal?&lt;/li&gt;
&lt;li&gt;32:57 - NFS/CIFS/SMB&lt;/li&gt;
&lt;li&gt;34:04 - SD Cards&lt;/li&gt;
&lt;li&gt;35:36 - exFAT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Links&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/71o6Tyh2MUk"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quais são todos os filesystems de todos os sistemas operacionais? Por que todos falam que ZFS é o melhor? E quais as alternativas mais modernas? Vamos ver tudo isso hoje!</p>
<h3>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>00:00 - Intro</li>
<li>00:32 - INode, DEntry</li>
<li>02:35 - VFS</li>
<li>03:18 - Journaling</li>
<li>05:54 - Filesystems com Journal</li>
<li>06:23 - Filesystems da Apple</li>
<li>07:59 - Filesystems de Linux</li>
<li>11:21 - Resumo</li>
<li>13:26 - ZFS</li>
<li>14:36 - checksums</li>
<li>16:28 - OpenZFS</li>
<li>18:11 - snapshots - Copy on Write</li>
<li>22:25 - de-duplication</li>
<li>23:46 - CoW vs Journal</li>
<li>24:49 - APFS</li>
<li>26:01 - compressão</li>
<li>26:32 - encriptação</li>
<li>29:04 - BTRFS</li>
<li>32:12 - o filesystem universal?</li>
<li>32:57 - NFS/CIFS/SMB</li>
<li>34:04 - SD Cards</li>
<li>35:36 - exFAT</li>
</ul>
<p>Links</p>
<ul>
<li>The Inode Object (<a href="http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec6.html"target="_blank" rel="noopener">http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec6.html</a>)</li>
<li>The Dentry Object (<a href="http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html"target="_blank" rel="noopener">http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html</a>)</li>
<li>The Superblock Object (<a href="http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec5.html"target="_blank" rel="noopener">http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec5.html</a>)</li>
<li>BSD - 9.4. Block Devices (Are Gone) (<a href="https://docs.freebsd.org/en/books/arch-handbook/driverbasics/#driverbasics-block"target="_blank" rel="noopener">https://docs.freebsd.org/en/books/arch-handbook/driverbasics/#driverbasics-block</a>)</li>
<li>ZFS on Ubuntu 20.04 LTS (<a href="https://ruvi-d.medium.com/zfs-on-ubuntu-20-04-lts-53728f3bc9e9"target="_blank" rel="noopener">https://ruvi-d.medium.com/zfs-on-ubuntu-20-04-lts-53728f3bc9e9</a>)</li>
<li>Apple abandons ZFS on Mac OS X project over licensing issues (<a href="https://arstechnica.com/gadgets/2009/10/apple-abandons-zfs-on-mac-os-x-project-over-licensing-issues/"target="_blank" rel="noopener">https://arstechnica.com/gadgets/2009/10/apple-abandons-zfs-on-mac-os-x-project-over-licensing-issues/</a>)</li>
<li>The Guts n’ Glory of Database Internals
B+Tree (<a href="https://ayende.com/blog/174402/the-guts-n-glory-of-database-internals-b-tree"target="_blank" rel="noopener">https://ayende.com/blog/174402/the-guts-n-glory-of-database-internals-b-tree</a>)</li>
<li>fs.h (<a href="https://github.com/torvalds/linux/blob/master/include/linux/fs.h"target="_blank" rel="noopener">https://github.com/torvalds/linux/blob/master/include/linux/fs.h</a>)</li>
<li>Anatomy of the Linux file system (<a href="https://developer.ibm.com/tutorials/l-linux-filesystem/"target="_blank" rel="noopener">https://developer.ibm.com/tutorials/l-linux-filesystem/</a>)</li>
<li>Linus - Deploying ANOTHER PETABYTE of Storage! (<a href="https://www.youtube.com/watch?v=fbipJUJLzpE"target="_blank" rel="noopener">https://www.youtube.com/watch?v=fbipJUJLzpE</a>)</li>
<li>A short history of btrfs (<a href="https://lwn.net/Articles/342892/"target="_blank" rel="noopener">https://lwn.net/Articles/342892/</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>No episódio anterior a gente viu o antigo FAT em detalhes e como ele evoluiu até o FAT32. Demos uma pequena olhada nos primórdios do NTFS, do HFS e como passamos a implementar filesystems com estruturas de árvore, os B+Trees. Vamos continuar exatamente de onde paramos pra falar dos principais filesystems que surgiram depois nos anos 90 até hoje. Vai ser uma longa história então vamos lá.</p>
<p>(&hellip;)</p>
<p>Pra começar, eu tava pensando em escovar bit do sistema de arquivos de UNIX e Linux, mas acho que o mais importante você já viu: que o conteúdo dos arquivos, a linguiça de bytes fica fatiado em cluster, e que as folhas de uma árvore B+Tree apontam pra esses clusters. A árvore em si tem nós com múltiplos filhos e cada nó é uma estrutura de dados que grava os metadados como nome de arquivo ou diretório e suas propriedades. No mundo Linux as duas estruturas mais importantes são os inodes ou index node e dentry ou directory entry.</p>
<p>Basta entender que inode e dentry são structs, interfaces. Se quiser saber mais sobre eles basta abrir o arquivo de cabeçalho fs.h no código fonte da kernel. Isto aqui do lado é uma versão simplificada. Um inode carrega os metadados que representam um arquivo ou um diretório. Veja por exemplo os campos de user id e group id que você normalmente modifica com comandos como <code>chown</code>. E olha aqui onde ele grava os timestamps. Você consegue ler esses metadados do inode usando o comando <code>stat</code> como eu fiz aqui, olha os metadados organizados de forma legível.</p>
<p>O nome do diretório ou arquivo fica na estrutura <code>dentry</code> que você pode ver aqui do lado simplificado. De importante é que o <code>dentry</code> também aponta pra uma estrutura mestra que é o <code>superblock</code>, que vai definir o filesystem em si. Pense nela como a raíz da árvore. Os <code>dentry</code> apontam pro superblock e pra inodes. E inode aponta pro <code>file</code> ou arquivo. E não só isso. Quando falamos que tudo em UNIX é um arquivo é porque arquivos propriamente ditos, dispositivos até named pipes são representados por inodes.</p>
<p>As diversas operações que podem ser feita em cima de inodes são definidos em outra estrutura, a <code>inode_operations</code>. Veja como ela tem ponteiros pra funções nomeadas como <code>create</code>, <code>mkdir</code>, <code>rmdir</code>. A kernel do Linux não fala diretamente com o filesystem. Ela fala com uma camada de abstração chamada VFS que é o virtual file system e ele fala com o filesystem.</p>
<p>Sistemas operacionais modernos aceitam diversos filesystems diferentes, como o Windows aceita NTFS ou FAT. Cada filesystem no Linux precisa preencher essa estrutura pra apontar pra funções específicas de sua implementação, mas todas devem lidar com essas estruturas como inodes ou dentry e assim se tornam compatíveis com Linux.</p>
<p>Se você for programador, o <code>inode_operations</code> é como uma interface implementada por uma classe num ext4 e os métodos dessa classe recebem objetos <code>inode</code> ou <code>dentry</code> pra trabalhar ir populando e gerenciando a árvore. Como você só vai ter que lidar diretamente com isso se estiver criando um novo filesystem ou módulos de kernel, saber só isso já deve ser suficiente pra continuar.</p>
<p>Um dos recursos que é normal hoje mas antigamente só se usava em máquinas corporativas, como mini-computadores AIX da IBM ou workstations Silicon Graphics, é Journaling. Muita gente nunca ouviu falar disso mas um journal ou um diário é tipo um log, implementado como um log circular. Circular pra não crescer pra sempre.</p>
<p>A gente costuma pensar que quando mandamos criar ou apagar um arquivo  é só Uma operação que acontece instantaneamente. Mas não é assim, são conjuntos de operações, como qualquer programa que você escreve: uma função formada por múltiplas instruções.</p>
<p>Por exemplo, apagar um arquivo num sistema de arquivos de UNIX e compatíveis vai ter pelo menos umas três operações. Primeiro precisa remover a estrutura de entrada de diretório da árvore. Um detalhe de implementação é que precisa devolver a estrutura de inode pro pool de inodes livres. E finalmente precisa liberar os blocos do disco pro pool de blocos livres.</p>
<p>Quando tem um crash, por bug ou pane elétrica, pode justamente parar no meio de uma dessas três operações e isso vai deixar nossas estruturas de dados num estado inconsistente ou corrompido. Por exemplo, a entrada de diretório foi removida da árvore mas o inode corresponde não foi devolvido pro pool de inodes. E assim por diante.</p>
<p>Por isso que em Linux antigo, se você bootasse a máquina apertando o botão de reset, sem dar shutdown, no próximo boot ele forçava rodar a ferramenta <code>fsck</code> que é o filesystem check. Que quem já viu sabe que é lento e você é forçado a esperar. Porque pra detectar onde que pode estar corrompido não tem jeito, precisa navegar nó a nó da árvore do sistema de arquivos e checar inode por inode.</p>
<p>Por isso inventaram a idéia de um journal. Um journal pode ser implementado de várias formas. Numa partição escondida, num arquivo escondido, num outro HD secundário separado. Enfim, a implementação vai variar dependendo do filesystem mas a idéia é primeiro escrever aquelas três operações nesse log e só depois começar a executar em cima da árvore de verdade. Se a operação for interrompida na metade, no próximo boot o sistema de arquivos pode checar as últimas operações no journal. Daí ele termina se puder, ou volta pro último estado indicado no journal.</p>
<p>De novo, é um trade off. A gente tá usando mais espaço em disco pra uma estrutura de suporte, ou seja, vai de novo ter um pouco menos de espaço livre pra você. Por outro lado temos menos chance de acabar com nosso sistema de arquivos corrompido e perder tempo tentando detectar o erro pra consertar. Num Windows antigo quando isso acontecia no boot ele carregava o Scandisk que é o equivalente <code>fsck</code> no Windows. Hoje você nunca vê ele.</p>
<p>Isso porque FAT32 não suportava journal, mas o NTFS que foi introduzido pelo Windows NT suporta. Essa era uma das grandes diferenças. O Mac também evoluiu seu HFS e lá pela versão 8 do antigo MacOS dos anos 90 criou o HFS Plus, que além de atualizar suas estruturas pra 32-bits também, passou a adotar um journal. Até poucas versões atrás o MacOS ainda usava HFS+ como filesystem principal, depois foi substituído pelo APFS, mas já falo dele.</p>
<p>Falando rapidamente em Macs, todo mundo sabe que depois de ter sido expulso da Apple no meio dos anos 80 o Steve Jobs fundou a NEXT pra produzir workstations UNIX e competir no mercado da Sun e Silicon Graphics. Pra isso o time dele criou o NEXTStep que era um UNIX no espírito de um Solaris ou IRIX. E usava o filesystem UFS como outros UNIX da época.</p>
<p>Durante os anos 90 a Apple foi decaindo sem parar. Passou na mão de vários CEOs e ninguém conseguiu recuperar a Apple. No meio dos anos 90, só 10 anos depois, todo mundo já tava contando os dias pra Apple declarar falência e fechar as portas. Daí o último CEO dessa geração, Gil Amelio fez a única coisa que dava pra fazer: convidou o Steve Jobs pra voltar. O resto é história.</p>
<p>Nessa época o sistema operacional dos Macs era o System 9. O System naquele ponto já tava praticamente perdendo pro Windows 98. Era ridículo como o software deteriorou em pouco mais de 10 anos, partindo da referência que todo mundo queria ser, pra um sistema legado e atrasado. Quando o Jobs voltou em 97 ele trouxe toda a propriedade intelectual da NEXT, incluindo o NEXTStep e toda a biblioteca orientada a objetos feito em Objective-C pra criar aplicativos modernos distribuídos em rede, que deixou até John Carmack babando.</p>
<p>Mas pra ficar compatível com os Macs, decidiram trocar o UFS, o Unix File System, pelo HFS, mais especificamente pelo HFS+ que foi lançado junto com o System 8.1 e adicionou suporte opcional a journaling. Até hoje se você for no Disk Utility dos MacOS, que é o equivalente ao Disk Management do Windows ou um GParted do Linux, vai ter a opção de formatar o volume com HFS+.</p>
<p>Como podem imaginar, journaling foi uma das grandes discussões da comunidade open source no fim dos anos 90. Distros Linux começaram usando o MINIX File System. MINIX que é o sistema operacional inspirado em UNIX criado pelo lendário professor Andrew Tanenbaum pra ensinar sistemas operacionais na faculdade. Já não se fazem mais professores como o Tanenbaum. O File system dele era muito bom, muito estável, mas tinha as limitações daquela época do começo dos anos 80 onde a gente economizava bytes, desperdiçando o menos possível, e por isso os nomes de arquivos só suportavam 14 caracteres de comprimento e partições de 64 megabytes.</p>
<p>A comunidade ajudou o Linux Torvalds e perto da versão 1.0 o MFS foi trocado pelo extended file system ou ext, que era inspirado um pouco no MINIX mas também no UFS que é o UNIX File System e seu sucessor que foi o Berkeley Fast File System ou BSD FFS. Se não me engano a gente ainda usa uma versão atualizada do FFS nos FreeBSD da vida.</p>
<p>O ext foi substituído pelo second extended filesystem. O ext2 foi o filesystem mais adotado no meio da década de 90 no mundo Linux. Comparado ao FAT do MS-DOS, era muito superior. Suportava volumes de até 32 terabytes, arquivos de 16 gigas até 2 tera, nomes longos de 255 caracteres e mais. Não devia nada ao HFS do Mac ou HPFS do OS/2.</p>
<p>No meio dos anos 90, acho que só o HPFS, o ext2 e o FAT32 é que ainda eram popularmente usados e não tinham um sistema de journaling. O OS/2 foi caindo em deuso e no fim dos anos 90 a IBM substituiu o HPFS pelo mesmo filesystem que usavam nos mini-computadores mais parrudos AIX, o JFS que significa justamente Journaling File System.</p>
<p>Os Silicon Graphics tinham o sistema operacional IRIX como falei alguns episódios atrás e o filesystem deles era um dos mais sofisticados, de alta performance com operações paralelas de I/O, extents, implementado com B+Trees e, claro, journaling. Esse era o XFS. Ambos XFS e JFS foram portados pra Linux no começo dos anos 2000. Por isso que você ainda vê eles por aí.</p>
<p>Em paralelo surgiram outros como o controverso ReiserFS que foi o primeiro filesystem com journaling que apareceu pra Linux, antes ainda do JFS e do XFS. Ele virou o filesystem padrão da distribuição SUSE que migrou pro ext3 em 2006. Eu imagino que uma das razões da migração foi o que aconteceu com o autor do ReiserFS, o Hans Reiser, que tinha fundado a empresa Namesys. Em 2006 ele foi preso e em 2008 foi condenado por ter assassinado a esposa, Nina Reiser. Depois disso ex-funcionários da Namesys deram continuidade ao ReiserFs junto com voluntários.</p>
<p>No fim dos anos 90 a adoção do Linux cresceu exponencialmente e as fraquezas do ext2 davam mais e mais dor de cabeça, especialmente a demora que era o <code>fsck</code> que era forçado a rodar no boot toda vez que você resetava a máquina sem dar shutdown. E de fato, vira e mexe você perdia inodes no caminho. Então fizeram um fork do ext2 e acrescentaram a funcionalidade de journal em cima, renomearam tudo pra ext3 e ele foi fácil de adotar porque era compatível com o ext2. Não precisava reformatar as partições, só carregar o novo filesystem. Então migrar era fácil.</p>
<p>Vamos dar uma pausa pra resumir um pouco. No mundo Microsoft a gente pulou dos anos 80 pros anos 90 evoluindo o maldito FAT, de FAT12 pra FAT16 pra VFAT e FAT32. Era essa porcaria que usávamos no Windows 95, 98 e Me. Depois da parceria do OS/2 com a IBM a Microsoft foi evoluindo em paralelo o Windows NT que era código novo feito do zero e diferente do Windows 95. Ele tinha o NTFS que, comparado com o FAT32, era anos luz à frente, suportava volumes muito maiores, arquivos muito maiores, nomes longos, sistema de permissão e tinha journaling. NTFS sobrevive até hoje.</p>
<p>No mundo Mac, depois do lançamento do revolucionário Macintosh em 1984 eles pularam do Macintosh File System ou MFS pro Hierarchical File System ou HFS, que já era baseado em B+Tree, suportava nomes longos, mas tinha limitações no tamanho de arquivos e partições. Era tipo um VFAT pouca coisa melhor. Isso ia melhorar em 1998 com o lançamento do System 8.1, a gente ainda estava alguns anos antes da Apple terminar de adaptar o NEXTStep pra virar o MacOS X que só seria lançado no ano 2000. Por quase 20 anos os MacOS iriam usar o HFS+.</p>
<p>No mundo Linux tinha a herança dos UNIX File System e isso derivaria no extended file system ou ext. Ele ia evoluir pro ext2 que foi o mais usado nos anos 90 e começo dos anos 2000. Como tava ficando atrasado outros começaram a aparecer como o ReiserFS que foi um dos primeiros a suportar journaling e outras funcionalidades que melhoravam a performance.</p>
<p>Ao mesmo tempo a IBM contribuiu abrindo o journaling file system ou JFS pro mundo Linux e além do OpenGL a Silicon Graphics, nos seus últimos dias, também abriu o XFS que usava nos IRIX pro mundo open source. Mas quem acabou sendo mais adotado, principalmente pela facilidade e compatibilidade, foi o ext3. A partir de 2006 começou o desenvolvimento do ext4 que só passou mesmo a ser adotado depois de 2010. Hoje o filesystem mais usado de Linux em desktops e notebooks provavelmente é o ext4 mesmo. Mas em servidores talvez seja um pouco diferente, como já vamos ver.</p>
<p>Esse é o panorama até os anos 2010. Não sei porquê, no mundo doméstico filesystems meio que estagnaram nos primeiros 15 anos do século XXI. Mas no mundo de data centers a coisa evoluiu diferente. Uma das últimas contribuições da antiga Sun e seu sistema operacional Solaris, baseado no DNA do UNIX System V, foi a introdução do filesystem ZFS, o Zetta File System. Se você nunca ouviu falar, devia procurar saber.</p>
<p>O ZFS foi definitivamente a próxima grande evolução na história de filesystems. Nenhum dos que falei até agora chega perto. Ele inicia a geração de filesystems que não enxergam só uma partição no HD mas é também um gerenciador de volumes ao mesmo tempo. Ele sozinho suporta criar um único volume em cima de vários HDs num pool ou piscina. A gente fala bastante o termo pool, mas pense nessa palavra como sendo um &ldquo;conjunto&rdquo; de alguma coisa. É só um nome mais fancy.</p>
<p>Enquanto tava todo mundo achando que ter journaling já era suficiente o ZFS tava preocupado com coisas maiores. O mercado da Sun e seu Solaris, principalmente depois que foi comprada pela Oracle, era equipar servidores que rodavam bancos de dados realmente massivos como a própria Oracle. Quando a Oracle comprou a Sun, ela continuou evoluindo o ZFS.</p>
<p>Muitos dos filesystem domésticos sequer incluiam checksums. Pra quem não sabe o que é um checksum recomendo assistir esse video do Ben Eater que vou deixar linkado nas descrições abaixo. Mas em poucas palavras é uma forma de certificar um bloco de bytes. Se alguém inexperiente quisesse saber se um bloco não foi corrompido talvez ele mantivesse pelo menos uma cópia exata desse mesmo bloco.</p>
<p>Mas isso seria um puta desperdício. Não dá pra saber qual dos dois blocos que tá certo ou se os dois tão errados, só que ficaram diferentes por alguma razão. Essa é a forma mais rudimentar de detectar erro mas desperdiçando o dobro de espaço. Em vez disso, o checksum é um número bem menor, que sim, consome alguns bytes a mais, mas é da ordem de 1% ou menos pra certificar um bloco de bytes e detectar erros. Pense em dígito verificador num código de barras.</p>
<p>O NTFS do Windows incrivelmente implementa checksums mas o HFS+ da Apple não e só no ext4 do Linux que passamos a ter checksums nos journals, que é crucial porque dependemos dele pra reconstruir as últimas operações no sistema de arquivos antes de uma pane. O ZFS vai além de checksums. Ao contrário do ext4 e outros derivados do UNIX file system, o ZFS não usa ferramentas como o <code>fsck</code>. Ele tem sistemas próprios pra diagnosticar erros e corrupção de dados.</p>
<p>Nenhum filesystem é imune a corrupção de dados, mas lendariamente pra você corromper um ZFS precisa ser defeito de hardware. E se estiver usando um pool de HDs, precisa acontecer erro catastrófico de hardware em múltiplos HDs. É bem mais difícil um erro acontecer no ZFS do que qualquer outro. Nenhum outro chega perto. Eu sei, tem outras opções, vou chegar lá, mas entenda que de 2005 em diante, se você quer montar um servidor de armazenamento sério de petabytes num datacenter, nunca vai usar HFS+ ou NTFS e nem ext4. Vai usar ZFS.</p>
<p>ZFS tá disponível também em FreeBSD e chegou a ser portado tanto pra Linux quanto Mac OS X. Mas no Mac eles deixaram de suportar. Aliás, por muitos anos a gente ficou discutindo quando a Apple ia oficializar o suporte a ZFS como filesystem principal. Alguns acreditam que o problema foi a Sun ter sido comprada pela Oracle, que deixou o resto da indústria meio insegura.</p>
<p>Quando a Oracle adquiriu a Sun em 2010 ela parou o desenvolvimento da versão open source do Solaris, o OpenSolaris. Muitos engenheiros originais da Sun se demitiram e iniciaram projetos paralelos como o OpenZFS, que tenta substituir todo o código proprietário do ZFS da Oracle, pra uma versão open source compatível com licenças de software livre.</p>
<p>No outro lado do espectro, a licença CDDL do ZFS e OpenZFS é um problema pro mundo Linux e GPL. Alguns dizem que GPL e CDDL não são compatíveis mas acho que ainda não tem um consenso. Na dúvida, o Linux não vem com OpenZFS. Você pode manualmente baixar e instalar, mas suportar ZFS em Linux sempre vai ser um problema até esse ponto de licenças ser resolvido. Como a licença BSD é menos restritivo, mais livre que o GPL, o CDDL não é um problema. Por isso que se você for montar servidores de armazenamento, é melhor preferir usar FreeBSD em vez de alguma distro Linux.</p>
<p>No mundo doméstico de Linux como Ubuntu, só recentemente passou a suportar, a partir da versão 19 ainda meio experimental. Hoje mais estável a partir da versão 20 do Ubuntu, dá pra instalar ZFS de forma mais fácil, direto do instalador. A Canonical não é da bandeira xiita de software 100% livre, então ela tem versões de Ubuntu que vem com coisas proprietárias como os drivers binários fechados da NVIDIA e agora o OpenZFS.</p>
<p>Além de checksums no nível de blocos e outras funcionalidades de auto-recuperação pra impedir corrupção de dados e dezenas de outras funcionalidades importantes que eu não conheço, a mais interessante mesmo pra usuários como nós é a capacidade do ZFS de ter snapshots baratos por conta do sistema de COW ou copy on write.</p>
<p>Agora fodeu. Vamos entender copy on write. Interessante que dos filesystems domésticos, nem o ext4 do Linux e nem o HFS+ da Apple tem suporte a snapshots baratos mas, de novo, o NTFS do Windows tem, se você tiver o serviço de Volume Shadow Copy ativado. Se for programador um sistema de copy on write é mais ou menos parecido em conceito com um commit de Git.</p>
<p>Como expliquei no começo, um arquivo no nível do sistema de arquivos é uma coleção de clusters ou blocos. Quando a gente edita um arquivo de texto e manda salvar, vai alterar só os blocos que foram modificados. O problema na maioria dos filesystems é que essa modificação é feita &ldquo;in-place&rdquo;, é literalmente escrever o dado novo &ldquo;em cima&rdquo; do bloco original. Se der bug ou falta de energia isso pode deixar o bloco original num estado corrompido.</p>
<p>Arquivos são divididos em clusters. Pra simplificar vamos imaginar que eu tenha um array que representa o arquivo. Cada posição do array aponta pra um cluster. Se meu arquivo tem 10 clusters, vamos ter um array de 0 a 9. Faz de conta que o nó da árvore que diz &ldquo;hello.txt&rdquo; aponta pra esse array.</p>
<p>Quero salvar uma modificação que vai alterar o conteúdo só dos cluster 5 e 6. Antigamente eu iria escrever os bits diretamente em cima desses clusters e pronto. Mas hoje eu posso fazer diferente. Primeiro eu faço uma cópia só desse array. Isso vai ser rápido porque ele é uma estrutura pequena só de endereços pros clusters. Agora eu escrevo o novo conteúdo em dois clusters novos. Feito isso basta alterar o endereço das posições 5 e 6 desse novo array pra apontar pros novos clusters.</p>
<p>O eu que eu tenho agora? Do ponto de vista do sistema de arquivos eu tenho dois arquivos com o mesmo nome, duas versões diferentes, mas eles não ocupam o dobro de espaço, porque os dois arrays apontam pra maioria dos mesmos clusters. Uma vez terminado, eu sei que o arquivo tá seguro, então eu vou na árvore e só mando trocar o ponteiro do array antigo pro array novo. Pronto, salvei meu arquivo de uma forma segura.</p>
<p>Mais do que isso. Porque eu preciso apagar o array antigo? E se eu chamar a estrutura do array antigo de versão 1 e a estrutura do array novo de versão 2? Essa é a raíz pra idéia de snapshots.</p>
<p>Faz de conta que você quer fazer uma atualização de sistema meio arriscada, talvez dê pau e você não consiga mais bootar o sistema. Antigamente a única opção que você tem é plugar um HD externo e fazer um backup de tudo. Dependendo do tamanho do seu HD isso pode levar horas. E se desse pra fazer uma cópia de tudo em segundos?</p>
<p>Um backup normal vai levar horas porque precisa copiar todos os bytes de todos os arquivos. Mas num sistema que suporta snapshots como eu expliquei, via copy on write, dá fazer uma cópia só da estrutura de árvore. Só os trechos modificados de cada arquivo que vai ocupar espaço novo. Não vai sobrescrever em cima. Se você tem 100 giga de arquivos e faz um snapshot, ter duas versões ocupando praticamente os mesmos 100 giga. Por isso falei que em conceito parece um commit de Git. Um commit só tem as linhas modificadas de um arquivo e não uma cópia do arquivo inteiro. Se não assistiram meu video de como Git funciona, recomendo ver depois.</p>
<p>Se é tão fácil assim porque não dá pra fazer isso em FAT32 ou HFS+? Por que as operações de escrita são in-place. Elas sobrescrevem em cima dos blocos antigos, pra economizar espaço. Mas como os HDs de hoje são ordens de grandeza maior, podemos copiar só os novos blocos pra um espaço livre, e manter os blocos originais sem tocar neles. Essa é a idéia por trás de garantir que você não vai acabar com dados corrompidos se crashear no meio do caminho. As versões originais nunca foram tocadas.</p>
<p>Snapshots são como commits de Git. E assim como Git você consegue voltar no tempo. Facilmente consegue recuperar diretórios ou arquivos apagados ou modificados, porque os blocos originais permanecem no disco. Obviamente vai ocupar mais espaço com o tempo, mas de novo, espaço em disco hoje é muito barato. A gente pode desperdiçar espaço em disco pra garantir que não vamos perder dados. E com o tempo podemos consolidar versões pra recuperar espaço.</p>
<p>Falando em duplicar, se você fizer uma cópia de um arquivo de um diretório pra outro diretório, iria realmente copiar bloco a bloco do arquivo original. Se o arquivo original for grande e tiver vários gigabytes, isso ia levar pelo menos alguns segundos e ocupar o dobro de espaço. Mas num sistema de copy on write vamos ocupar zero bytes a mais. Tem até um processo de deduplicação pra quando for copiar um arquivo ele vê se já não tem outro com os mesmos bytes, daí só aponta pra ele.</p>
<p>Como o ZFS tem copy on write por bloco, enquanto ninguém mexer nos dois arquivos, eles vão apontar pro mesmo linguição de bytes. A vantagem é que vai custar tempo zero e espaço zero pra criar a duplicata. Só se um dos arquivos for modificado que realmente vamos precisar ocupar espaço novo só dos blocos que foram modificados.</p>
<p>A mesma coisa acontece na memória quando o Linux faz um fork de um processo, ou seja, cria uma duplicata de um processo pra usar os diversos cores de uma CPU moderna. Se fosse uma cópia exata, e o processo estiver ocupando 100 mega de memória, cada vez que fizermos uma cópia deveria duplicar a memória do processo novo. Se fizermos 3 forks, deveria ocupar 300 mega de memória a mais. Mas no momento do fork, o processo original e os 3 forks vão todos ocupar os mesmos 100 mega originais. Só quando o processo voltar a rodar e o programa começar a modificar a memória, que só esses bytes modificados vão ser copiados pro espaço do processo novo e serem modificados.</p>
<p>E como copy on write não altera os blocos originais, você não corre risco de corromper os dados originais durante uma operação se cair a luz. Os blocos originais permanecem no mesmo lugar. Significa que um sistema de copy on write bem implementado descarta a necessidade um journal tradicional.</p>
<p>Não é que ZFS não tem um journal, mas ele é superior a só um mero log circular. Além das provisões anti-corrupção com checksums por bloco,  pode ser configurado pra realmente manter múltiplas cópias, usar algoritmos de checksum melhores como dedup, influenciar como requisições síncronas são feitas e controlar o cache pra balancear performance, mas com algum risco de perder o que tá no cache como já expliquei.</p>
<p>O ZFS olha pra um NTFS ou HFS+ e cospe no chão. Porém o ZFS é tudo menos de fácil. Usabilidade pra usuários não técnicos é baixa. Se você configurar o ZFS errado vai sim perder dados ou corromper tudo. É aquela velha história, você pode configurar todos os aspectos de como o ZFS funciona, mas se mandar ele acelerar e bater na parede, é isso que  vai acontecer.</p>
<p>Mesmo assim, muita gente gostaria de ver o ZFS no MacOS e por anos ficou no ar essa expectativa. Mas isso nunca se materializou. A Apple não gosta de arriscar. E com o controle da Oracle ela desistiu do ZFS e começou a desenvolver um novo filesystem dentro de casa mesmo. Isso levou ao APFS que saiu só em 2017 no iOS 10.3 e macOS 10.13 High Sierra.</p>
<p>Não é um ZFS mas pelo menos é moderno. Eu chutaria que supera o NTFS do Windows. De cara ele tem essa funcionalidade de cópias baratas e deduplication usando delta extents. Se você tentar copiar um diretório gigante, vai ser barato porque não vai duplicar os bytes do conteúdo.</p>
<p>Melhor que isso, APFS implementa snapshots sem fazer cópia de todos os diretórios e arquivos, parecido com o que expliquei do ZFS. O Time Machine da própria Apple que é o sistema de backup deles meio que já tentava fazer isso usando o recurso de hard links. Isso é em nível de arquivo, ZFS é no nível de bloco.</p>
<p>Além disso o APFS incluiu recursos que o NTFS já tinha como encriptação moderna, integridade de dados via checksums, proteção contra crashes usando uma redirect-on-write que é parecido com copy on write e por último compressão dos dados.</p>
<p>Eu nem mencionei ainda, mas compressão é uma coisa que eu particularmente não curto muito. Acho que não compensa o trabalho nem o risco. NTFS e ZFS também tem suporte a compressão. Talvez valha a pena se a maioria dos arquivos no sistema for fácil de comprimir como arquivos de texto, que desperdiçam muito espaço. Mas coisas que já são comprimidas como imagens JPG, videos com codecs que já são comprimidos como H.265 ou NVENC, o benefício é muito pequeno. Se você não sabe que precisa de compressão então não precisa de compressão.</p>
<p>É diferente de encriptação, isso eu recomendo que todo mundo ative, especialmente em notebooks que podem ser facilmente roubados num aeroporto da vida. A última coisa que você precisa é que alguém acesse seus dados. Ter uma senha não é suficiente porque dá pra montar um HD em outro computador e ler os dados. Macs sempre tiveram o FileVault em cima do HFS+ mesmo, NTFS tem o sistema BitLocker e Linux tem o LUKS. Então todos os principais sistemas operacionais suportam encriptação e você deveria ligar.</p>
<p>Como a maioria das CPUs modernas tem instruções de encriptação como AES implementados em hardware a perda de performance é imperceptível. Então perda de performance nem é uma boa desculpa. Só se lembre de não esquecer a senha, porque se esquecer é impossível recuperar seus dados. A única boa desculpa pra não encriptar seus volumes é se você for uma desgraça pra gerenciar chaves.</p>
<p>Isso me lembra de outro recurso de sistemas de arquivos que é safe delete. Não sei se você já consegue intuitivamente entender, mas um arquivo é dividido em múltiplas estruturas. Uma coisa é uma struct como dentry e inode que vão guardar coisas como o nome do arquivo, datas de modificação criação, permissões, etc e outra coisa é o data fork, o linguição de bytes do conteúdo do arquivo propriamente dito.</p>
<p>Quando você apaga um arquivo, é rápido porque só tem que apagar o inode da árvore e ponto. Porém os bytes do arquivo em si vão continuar no disco. Só não ter nenhuma estrutura apontando pra eles. Se você tiver treinamento forense vai conseguir ler os bytes crus do HD e pode achar dados não encriptados em arquivos texto. Se você guardou senhas num arquivo de texto e depois apagou, esse conteúdo ainda vai estar no disco. Por isso sempre se fala pra não guardar senhas em arquivos.</p>
<p>A única forma de garantir que os dados em si foram apagados é sobrescrever bits por cima dos bits originais múltiplas vezes. Se você sobrescrever os bits do conteúdo, de fato tá apagando os dados. E precisa ser múltiplas vezes por uma característica de discos magnéticos de HDs mecânicos de manter as camadas magnéticas antigas embaixo das novas.</p>
<p>De novo, se você for um técnico forense com técnica e equipamento adequado, consegue &ldquo;raspar&rdquo; os bits da camada mais nova e tentar ler os bits da camada de baixo. Por isso que um safe delete vai sobrescrever todos os bits do arquivo múltiplas vezes. E por isso é demorado e quase ninguém faz. Mas se seu HD estiver encriptado, basta perder a chave. Os dados já eram invioláveis porque os bits tão bagunçados e sem a chave não dá ler, então não precisa sobrescrever nada.</p>
<p>Além do APFS pra Mac, o mundo Linux também ganhou uma opção mais moderna. Como teve toda essa controvérsia da Oracle, licenças e tudo mais, apesar de existir uma versão aberta de OpenZFS pra Linux, a adoção é baixa. Só distribuições focadas em serem instaladas em servidores de arquivos que usam. No mundo doméstico, só agora com um Ubuntu 20 que apareceu a opção de modo fácil de instalar.</p>
<p>Mas toda essa controvérsia abriu caminho pra um projeto novo e mais moderno, o btrfs que se lê como butter, de manteiga, embora eu sempre achasse que era better, de &ldquo;melhor&rdquo;. Mas <code>btr</code> tem a ver com a estrutura de dados que usa por baixo, que é um B+Tree melhorado. Tem um paper que explica a inovação dessa nova estrutura de dados. Lembra toda a movimentação de nós que precisa fazer pra rebalancear uma árvore? Essa versão o interessante parece ser que ele precisa fazer bem menos operações de rebalanceamento.</p>
<p>O importante é que o btrfs também é um filesystem de nova geração como o ZFS, que ao mesmo tempo é um filesystem e um gerenciador de volumes. E também como o ZFS permite criar volumes em cima de um pool de HDs. É implementado com copy on write, permite deduplicação de dados e snapshots baratos.</p>
<p>Assim como o OpenZFS, poucas distros suportam btrfs desde a instalação. O Ubuntu 20, além de OpenZFS também passou a suportar btrfs na instalação. O OpenSUSE já vinha suportando também. A Suse tem histórico de adotar filesystems mais modernos mas possivelmente mais instáveis como foi o caso do ReiserFS no começo do século. O Fedora 33 também começou a adotar em 2020 então estamos bem num ponto de inflexão onde o btrfs tá estável o suficiente pra ser mais adotado.</p>
<p>O fato é que o ext4 é compatível com o ext3 que é compatível com o ext2. É um dos códigos mais antigos da história do Linux. Isso facilitou adoção mas adicionar funcionalidades novas foi ficando mais e mais difícil e mesmo os desenvolvedores do ext4 concordam que já chegou num limite e acham que coisas mais avançadas só vão ser possíveis em cima do btrfs.</p>
<p>Não tem mais nenhum outro filesystem no Linux com potencial porque o OpenZFS precisaria resolver esse problema de licenças e tudo mais. O JFS da IBM ou o XFS da Silicon Graphics também são antigos e pouca gente tem interesse em dar suporte. APFS é proprietário só da Apple e NTFS continua proprietário só da Microsoft. Só sobrou mesmo o btrfs.</p>
<p>Eu comecei a usar btrfs no meu Arch Linux Manjaro. E embora o Manjaro não dê suporte total na instalação, foi bem fácil configurar a partição manualmente. E uma vez instalado é transparente pro Linux e funciona muito bem. Essa transparência é muito porque todo sistema operacional moderno hoje funciona com um VFS ou virtual file system, como falei no começo.</p>
<p>Dessa forma você não precisa fazer aplicações específicas pra cada filesystem e por isso o Nautilus do GNOME que é o equivalente do Explorer de Windows ou Finder de Mac vai conseguir realizar as operações básicas como listar diretórios, copiar arquivos, renomear diretórios e coisas assim em qualquer filesystem, seja ext4 ou btrfs.</p>
<p>Mas pra fechar a história ainda faltou falar do último filesystem. Eu disse que o APFS e mesmo o HFS+ antes dele são proprietários da Apple, sem código aberto. Então, se você formatar um pendrive em APFS, na prática só vai conseguir ler em outro Mac. O mesmo vale pro NTFS da Microsoft. Hoje em dia a gente até consegue pelo menos ler dados de NTFS em Mac e Linux, mas escrever ainda é instável porque os drivers não são suportados pela Microsoft e nunca se sabe quando uma atualização do NTFS vai deixar volumes que dependem do driver open source corrompidos.</p>
<p>Por outro lado, todo filesystem de Linux é aberto mas nem Apple e nem Microsoft tem interesse em dar suporte nativo no MacOS ou Windows. Ou seja, se você precisa transferir dados entre diferentes sistemas operacionais, como faz?</p>
<p>A forma mais fácil talvez seja usando NFS que é o network file system. É como você se conecta a pastas compartilhadas na rede local. NFS é um protocolo pra filesystems distribuídos em rede, desenvolvido pela Sun. Pois é, a Sun foi um dos desenvolvedores mais influentes dos anos 90 e 2000, uma pena que morreu. Como é código aberto, praticamente todo mundo suporta NFS.</p>
<p>O mundo Windows obviamente tem protolos proprietários como SMB e CIFS. CIFS é o sucessor do antigo Samba. A menos que você precise se conectar em algum servidor antigo em Windows 2000, vai usar CIFS. SMB é o Server Message Block mas a gente conhece como Samba mesmo, é bugado, tem buracos de segurança e não é recomendado. CIFS é o Common Internet File System. Mesmo em Linux e Mac dá pra se conectar com esse protocolo.</p>
<p>Felizmente nunca precisei lidar muito com nenhum deles mas meu feeling é que NFS tende a simplesmente funcionar e CIFS vira e mexe tem probleminhas. Mas hoje em dia, pra maioria da população, ambos funcionam bem o suficiente pra copiar arquivos entre seu PC e seu notebook por exemplo.</p>
<p>Mas e pendrives? Pendrives são dispositivos USB com chips de armazenamento com memória flash. Além de HDs externos que são basicamente caixinhas com adaptadores SATA pra USB, temos pendrives e SD cards. E SD cards variam em qualidade. Os melhores são os SDXC UHS. Se você não é da área de video nem deve saber que existia diferença. Mas SD card e mini SD card tem classes diferentes de velocidade.</p>
<p>A classe 2 é a mais lenta, nem sei se ainda existe porque é só de 2 mega por segundo, horrível de lento. A Classe 4 é 4 mega por segundo. Classe 6 é 6 mega por segundo. Classe 10 é 10 mega por segundo. Eu pessoalmente acho 10 mega por segundo horrível, mas a maioria dos SD card baratos que você achar podem ser de 6 mega ou menos.</p>
<p>Os realmente bons, que qualquer câmera de 4K vai exigir, são os SDXC UHS que é Ultra High Speed. E mesmo assim não é tão ultra high assim. O UHS 1 ou U1 são os mesmos 10 mega por segundo da classe 10. O top de linha são os UHS 3 ou U3 que são 30 mega por segundo de velocidade de gravação sequencial.</p>
<p>Assim como no caso de USB que tem o tipo de conector como USB Type-C e USB, o protocolo, como 3.2 Gen 2, ainda tem os tipos de barramento de SD Card. Normal é pras classes 2 até 6. High Speed é pra clase 10, e UHS-I é pra U1 e UHS-II é pra U3. Na prática é simples, se você for amador, classe 6 é mais que suficiente. Se for profissional, pára de fazer economia porca e compra SDXC U3. Senão não tem como gravar 4K acima de 60 quadros por segundo.</p>
<p>Outro dia quero tentar explorar mais sobre video. Mas de volta a SD Cards e pendrives. Que filesystem vamos usar neles? Qualquer um serve, mas se quiser usar o pen drive pra copiar arquivos de um Mac pra Windows ou Linux, existe algum filesystem que funciona em todos?</p>
<p>Ironicamente tem, e é um derivado do FAT32. Sei lá porque a Microsoft resolveu ser bonzinha com o mercado a abriu a especificação do FAT e criou o exFAT ou extensible FAT em 2006. Provavelmente pelo crescimento do mercado de multimidia, de câmeras fotográfica e video. Ela viu que ninguém tava a fim de adotar o NTFS. Antes que acabassem adotando outro padrão, ela escolheu o menor de dois maus e criou o exFAT como padrão aberto.</p>
<p>O exFAT é um filesystem com cheiro de mofo desde o nascimento. É o pior em termos de funcionalidades comparado com todos que expliquei hoje. Perto de um ZFS ou btrfs ou mesmo o APFS, o exFAT é um neandertal andando nos tempos modernos.</p>
<p>Mas isso não importa. Um filesystem de dispositivos pra gravação de câmera não precisa de gerenciamento de volumes e pools de storage. Não precisa de compressão nem de encriptação porque velocidade é prioridade. E não tá nem aí pra journaling, copy on write ou qualquer coisa assim. Um dispositivo pra câmera precisa só ser muito rápido e suportar arquivos muito grandes. Só. Porque logo depois da gravação os arquivos vão ser copiados pra um servidor de ingestão de video e o SD card ou HD externo vai ser apagado pra próxima gravação.</p>
<p>Ninguém vai manter nada importante em exFAT por muito tempo. Os arquivos só precisam durar o suficiente pra serem transferidos pra outro lugar depois, como um servidor rodando OpenZFS. E lá sim, precisa de todas as garantias de redundância que já expliquei. Mas numa câmera? exFAT suporta volumes de até 16 exibites teóricos, e nem existe um SD card perto disso. E suporta arquivos de até 16 exibites também. Ou seja, não tem nenhuma garantia que semana que vem seus arquivos vão estar lá, mas foda-se, não precisa durar tudo isso, só precisa ser rápido.</p>
<p>E a ironia é isso: o exFAT sendo o filesystem o descendente dos anos 80 e não tendo implementado nenhuma funcionalidade moderna, ficou rápido hoje. Ele é simples de dar suporte. Não tem nada complicado como gerenciador de volumes ou encriptação, é só o bom e velho FAT só que sem limites de espaço e tamanho de arquivos. Sendo simples assim, ficou fácil dar suporte e por isso além de Windows tanto a Apple quanto Linux passaram a dar suporte.</p>
<p>Interessante como o único filesystem universal hoje é o exFAT. Se você vai transferir arquivos entre máquinas com sistemas operacionais diferentes ou com coisas como câmeras, provavelmente vai usar SD cards ou HDs externos formatados em exFAT.</p>
<p>E aqui termina nossa jornada sobre dispositivos de armazenamento e sistemas de arquivos. A intenção foi dar contexto de como as coisas evoluíram, como uma se relaciona com a outra e porque são importantes. Espero que isso tenha ajudado a dar uma luz nessa sopa de letrinhas. Se ficaram com dúvidas mandem nos comentários abaixo. Assinem o canal, cliquem no sininho, compartilhem o video pra ajudar o canal. A gente se vê, até mais.</p>
]]></content:encoded><category>armazenamento-e-backup</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #102 - Entendendo Sistemas de Arquivos: FAT</title><link>https://akitaonrails.github.io/2021/07/20/akitando-102-entendendo-sistemas-de-arquivos-fat/</link><guid isPermaLink="true">https://akitaonrails.github.io/2021/07/20/akitando-102-entendendo-sistemas-de-arquivos-fat/</guid><pubDate>Tue, 20 Jul 2021 11:37:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/6YQve3KUqJs"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Como seus programas sabem onde encontrar arquivos no HD? Como diretórios e arquivos são organizados?&lt;/p&gt;
&lt;h3&gt;Conteúdo&lt;span class="hx:absolute hx:-mt-20" id="conteúdo"&gt;&lt;/span&gt;
&lt;a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;00:47 - Revisando sobre Cartuchos&lt;/li&gt;
&lt;li&gt;03:46 - Sprites em Arquivos?&lt;/li&gt;
&lt;li&gt;06:24 - Primeiros Sistemas de Arquivos&lt;/li&gt;
&lt;li&gt;09:12 - Anos 80: Tudo incompatível&lt;/li&gt;
&lt;li&gt;09:34 - Revisando MBR&lt;/li&gt;
&lt;li&gt;12:55 - FAT12&lt;/li&gt;
&lt;li&gt;15:42 - FAT16&lt;/li&gt;
&lt;li&gt;16:37 - Windows 95 - Nomes Longos&lt;/li&gt;
&lt;li&gt;17:48 - Introdução a Bit Vector&lt;/li&gt;
&lt;li&gt;19:28 - VFAT&lt;/li&gt;
&lt;li&gt;21:09 - FAT32&lt;/li&gt;
&lt;li&gt;22:03 - Microsoft e IBM&lt;/li&gt;
&lt;li&gt;23:23 - IDE, SATA, AHCI, etc&lt;/li&gt;
&lt;li&gt;25:15 - HPFS ao NTFS e HFS&lt;/li&gt;
&lt;li&gt;25:53 - B+Tree&lt;/li&gt;
&lt;li&gt;27:28 - Resource Fork e Alternate Streams&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Links&lt;span class="hx:absolute hx:-mt-20" id="links"&gt;&lt;/span&gt;
&lt;a href="#links" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Design of the FAT file system (&lt;a href="https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system"target="_blank" rel="noopener"&gt;https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A Disk Editor View of the NTFS Boot Sector and &amp;ldquo;Bootstrap Code&amp;rdquo; for Windows™ 2000 and XP (&lt;a href="https://thestarman.pcministry.com/asm/mbr/NTFSbrHexEd.htm"target="_blank" rel="noopener"&gt;https://thestarman.pcministry.com/asm/mbr/NTFSbrHexEd.htm&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;FAT (&lt;a href="https://www.win.tue.nl/~aeb/linux/fs/fat/fat-1.html"target="_blank" rel="noopener"&gt;https://www.win.tue.nl/~aeb/linux/fs/fat/fat-1.html&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;FAT (&lt;a href="https://wiki.osdev.org/FAT"target="_blank" rel="noopener"&gt;https://wiki.osdev.org/FAT&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;SCRIPT&lt;span class="hx:absolute hx:-mt-20" id="script"&gt;&lt;/span&gt;
&lt;a href="#script" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Olá pessoal Fabio Akita&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/6YQve3KUqJs"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Como seus programas sabem onde encontrar arquivos no HD? Como diretórios e arquivos são organizados?</p>
<h3>Conteúdo<span class="hx:absolute hx:-mt-20" id="conteúdo"></span>
    <a href="#conte%c3%bado" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>00:00 - Intro</li>
<li>00:47 - Revisando sobre Cartuchos</li>
<li>03:46 - Sprites em Arquivos?</li>
<li>06:24 - Primeiros Sistemas de Arquivos</li>
<li>09:12 - Anos 80: Tudo incompatível</li>
<li>09:34 - Revisando MBR</li>
<li>12:55 - FAT12</li>
<li>15:42 - FAT16</li>
<li>16:37 - Windows 95 - Nomes Longos</li>
<li>17:48 - Introdução a Bit Vector</li>
<li>19:28 - VFAT</li>
<li>21:09 - FAT32</li>
<li>22:03 - Microsoft e IBM</li>
<li>23:23 - IDE, SATA, AHCI, etc</li>
<li>25:15 - HPFS ao NTFS e HFS</li>
<li>25:53 - B+Tree</li>
<li>27:28 - Resource Fork e Alternate Streams</li>
</ul>
<h3>Links<span class="hx:absolute hx:-mt-20" id="links"></span>
    <a href="#links" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Design of the FAT file system (<a href="https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system</a>)</li>
<li>A Disk Editor View of the NTFS Boot Sector and &ldquo;Bootstrap Code&rdquo; for Windows™ 2000 and XP (<a href="https://thestarman.pcministry.com/asm/mbr/NTFSbrHexEd.htm"target="_blank" rel="noopener">https://thestarman.pcministry.com/asm/mbr/NTFSbrHexEd.htm</a>)</li>
<li>FAT (<a href="https://www.win.tue.nl/~aeb/linux/fs/fat/fat-1.html"target="_blank" rel="noopener">https://www.win.tue.nl/~aeb/linux/fs/fat/fat-1.html</a>)</li>
<li>FAT (<a href="https://wiki.osdev.org/FAT"target="_blank" rel="noopener">https://wiki.osdev.org/FAT</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal Fabio Akita</p>
<p>Depois dos últimos episódios espero que vocês tenham entendido mais sobre como HDs são organizados em partições e volumes e como seu computador boota e acha o sistema operacional instalado no HD. Hoje quero explicar o que tem dentro dos volumes do HD: os sistemas de arquivo ou filesystems.</p>
<p>Vou começar contando um pouco o que me lembro. De como a vida era difícil nos PC dos anos 80 e 90 com o maldito filesystem FAT, que corrompia fácil. Apesar de muita gente não gostar, o NTFS do Windows NT foi um adianto de vida enorme. No Linux a gente tinha a série ext de filesystems. Nos Macs tinha HFS. Mas qual a diferença?</p>
<p>(&hellip;)</p>
<p>Antes de começar o assunto eu queria voltar de novo alguns episódios pra trás. Vocês já devem ter notado que fico insistindo nos videos de introdução a computação com videogames e mencionar cartuchos. Quando falei de cartuchos pela primeira vez foi só pra chegar nessa introdução de hoje.</p>
<p>Vocês lembram que nos chips da ROM a gente guarda as instruções do jogo em si mas numa região especial tem todos os assets do jogo, as imagens ou tiles, que formam os sprites dos personagens, por exemplo. O intervalo de endereços que definem a área CHR, que provavelmente quer dizer characters. Sem mappers só tem um intervalo pequeno de 8 kilobytes do endereço 0000 até 1FFF.</p>
<p>Sprites, como do Super Mario, são formados por tiles ou azulejos. Quatro tiles formam o Super Mario. Cada tile com 8 por 8 pixels. Então o Mario inteiro são 16 por 16 pixels. Vamos fazer umas continhas de padeiro. Sem nenhum mapper, pode ter no máximo 8 kilobytes ou 8 mil cento e noventa e dois bytes. Acabei de falar que cada tile tem 8 por 8 pixels. No Super Mario conseguiram entuchar quinhentos e doze tiles.</p>
<p>Portanto, cada pixel só pode ser representado por 2  bits de cor. E pra um NES é mais que suficiente. Hoje em dia, numa imagem normal, se representa cada pixel com pelo menos 24-bits, 8-bits pra cada cor. Se você já mexeu com HTML e CSS, já deve ter escrito uma cor em 24-bits.</p>
<p>Por exemplo FFFFFF é branco. O primeiro FF é o hexadecimal de 256, e representa vermelho. O segundo FF é pra verde e o terceiro é pra azul. Os três no valor máximo de 8-bits que é FFFFFF, vai dar branco. FF0000 significa vermelho no máximo e zero pra verde e azul.</p>
<p>Hoje em dia  tem bem mais cores que isso, em vez de 24-bits podemos usar 30 bits, onde cada uma das 3 cores básicas pode ser representada com 10-bits. Por isso, quando você vê um monitor ou TV moderna dizendo HDR-10 é High Dynamic Range com cores de 10-bits. Mas cores é assunto pra outro dia. O problema é o seguinte: se um tile de nintendinho fosse representado com esse tanto de cor, 30-bits, um tile de 8 por 8 pixels seria 8 vezes 8 vezes 30, ou seja mil noventos e vinte bits. Divide por 8 pra ter em bytes e isso seria duzentos e quarenta bytes.</p>
<p>Só que aí, nos nossos pobres 8 kilobytes de ROM no cartucho, só caberia trinta e quatro tiles. Pra fazer caber quinhentos e doze, o máximo que podemos usar pra cores são 2 bits, meio byte. E por isso em videogame antigo tinha uma outra estrutura de dados que são as paletas de cores. Note no debugger do emulador Mesen que embaixo do zoom do tile tem uma estrutura de 4 cores, essa é a paleta atual.</p>
<p>A gente pode mudar a paleta e as cores dos tiles vão mudando. É assim que se consegue reusar os tiles que formam a nuvem e tem cores como azul e branco e pra fazer o arbusto, basta trocar a paleta pra verde. Olhem aqui no nosso debuger como esse tile tá verde, mas quando aparece a nuvem na tela, muda pra branco. É o mesmo tile, só muda a paleta.</p>
<p>Sem detalhar porque, o nintendinho tinha uma paleta total de 54 cores mas cada tile só podia escolher entre uma paleta que tem no máximo 3 cores, mais o preto, então 4 cores, representado por 2-bits. Isso faz cada tile de 8 por 8 pixels ocupar 32 bytes e num cartucho simples, sem mapper, do endereço 0000 até 1FFF, cabe no máximo quinhentos e doze tiles.</p>
<p>Se a gente fosse fazer esse joguinho do super mario num computador moderno, como você faria? Um jeito ingênuo, já que não usamos mais cartuchos, seria gravar cada tile em um arquivo, tipo tile1.png, tile2.png, etc. Cada arquivo contendo exatamente os 32 bytes de cada tile. A parte divertida é que o arquivo na verdade vai consumir mais que 16 bytes. Por que?</p>
<p>Porque só o nome do arquivo &ldquo;tile1.png&rdquo; tem 18 bytes (em double byte, letras em UNICODE, lembra?). Só o nome já aumenta quase 50% o tanto de bytes. E não acaba aí. Em qualquer gerenciador de arquivos você consegue ver coisas como a data de modificação do arquivo. Precisa guardar esses bits, e digamos que ocupe 1 byte ou mais. E tem que guardar em que diretório esse arquivo tá, então no mínimo mais um ponteiro de endereço pra estrutura de diretório.</p>
<p>E na realidade tem mais metadados que isso, então cada arquivinho de 32 bytes vai ter quase o triplo de bytes pros metadados. Num Linux, na realidade, é mais que isso. Cada arquivo tem 2 kilobytes de metadados e estruturas. Os metadados são maiores que esse conteúdo. Por isso que você não deveria tentar guardar uma coisa tão pequena num arquivo. Claro que ninguém faz isso, mas só pra explicar caso você nunca tenha pensado nisso: metadados custam bytes e não são poucos. O tamanho de um arquivo não é só o tamanho do conteúdo.</p>
<p>Se o cartucho de Nintendo usasse um sistema de arquivos como um ext4 de Linux moderno, a gente não ia conseguir colocar quase nenhum tile nele. Só de metadados inúteis já ia encher. Aliás, a configuração do ext4 não ia caber no cartucho. E por isso que um cartucho não usa sistema de arquivo. Nenhum tile tem nome nem nada, só direto o endereço. Sistema de arquivos substitui a necessidade de lidar com endereços, ao custo de adicionar peso pra dar nome e organização pras coisas.</p>
<p>Se eu fosse simular um cartucho numa linguagem moderna, os bytes de tiles, guardaria num array. E se fosse gravar num dispositivo, gravaria todos os bytes crús juntos num arquivão só. Aliás, é isso que é uma imagem de ROM de cartucho que você roda num emulador: todos os bytes crus do cartucho. Mas aí, como você ia carregar só o tile correto? Igual se fazia antigamente: indo na posição desse arrayzão onde começa o tile e mandando ler só 32 bytes.</p>
<p>Quando a gente espeta um cartucho num videogame ou num microcomputador antigo como um Commodore, é o equivalente a espetar mais memória já pré-carregada. Quando executa um jogo num PC que tá gravado num disquete ou mesmo HD, primeiro ele carrega o jogo na memória, então tem um passo a mais e gasta a RAM. Sistema de arquivos aparece quando a gente passa a ter um dispositivo de armazenamento, de onde vamos carregar dados pra memória pra depois usar. Não usamos os bytes direto do dispositivo porque é lento demais.</p>
<p>É a transição da fase que computador só tinha memória e expandia com cartuchos. Daí passamos a usar gravador de fita cassette pra guardar os bytes do programa, literalmente uma fita de bytes. Um Commodore 64 usava um sistema de arquivos bem rudimentar, basicamente uma lista ligada com a coordenada de trilha e setor, 2 bytes pro próximo bloco e duzentos e cinquenta e quatro bytes de dados. Acho que era uma tabela de nomes de arquivos e cada arquivo sendo uma lista ligada assim.</p>
<p>O problema é que só dava pra escrever sequencialmente, e o próximo arquivo começava a partir do último endereço do último arquivo mais um byte e seguia assim. Se apagasse um arquivo, aquele espaço no meio não dava pra ser reusado automaticamente sem usar um comando como MOVE pra literalmente mover os arquivos que tavam depois pra ocupar os bytes antes.</p>
<p>Quando pensamos em dados, seja um único arquivo ou vários arquivos, na nossa cabeça a gente pensa como num cartucho: tudo empacotado um atrás do outro bonitinho. E no fim dos anos 70, no mundo doméstico, muitos computadores eram usados assim: você plugava um cartucho de jogo, jogava e desligava a máquina. Fazia uma conta de calculadora, e desligava a máquina.</p>
<p>Carregava um jogo de uma fita cassette, jogava, e desligava a máquina. E toda vez que desligava, tudo que tava na memória RAM apagava e era isso aí. Videogames até a era de um super nintendo, não gravavam nada em lugar nenhum. Com exceção de cartuchos que dava pra salvar o progresso do jogo, que era basicamente um chip de RAM ligado numa bateria daquelas de relógio dentro do cartucho.</p>
<p>Sistema de arquivos era mais comum num computador comercial, um mainframe ou minicomputador, rodando UNIX, gravando as coisas em rolos de fita gigantes ou nos primeiros HDs, que também eram gigantes, suportavam só alguns kilobytes e custavam uma fortuna. Mas foi só no começo dos anos 80 que coisas como disquetes e fitas cassete começaram a aparecer.</p>
<p>Daí surgiram os primeiros programas de produtividade, como eu já tinha mencionado em outros episódios, editores de texto como Wordstar ou Wordperfect, planilhas como Lotus 1-2-3 ou Visicalc, bancos de dados como dBase. Você ficou o dia todo criando uma planilha de custos, precisava gravar esses dados em algum lugar. Não podia ser como antes que ia desligar a máquina, perder tudo e ter que redigitar tudo no dia seguinte. Daí surge a necessidade de sistemas de arquivos que começaram bem rudimentar em micros como MSX ou Commodore.</p>
<p>Nos anos 80, cada microcomputador como Apple II, Commodore e tudo mais usava um sistema de arquivos proprietário. Trocar arquivos entre micros era um saco. Além do sistema de arquivos, no caso de arquivos texto, nem todo mundo usava ASCII, então mesmo conseguindo copiar pra outra máquina, quando abrisse pra editar vinha caracteres estranhos porque cada um usava códigos diferentes pra símbolos diferentes.</p>
<p>Com a dominância dos clones de IBM PC e todo mundo rodando ou MS-DOS ou um clone de MS-DOS, meio que estabilizou que o sistema de arquivos mais comum seria FAT e a tabela de caracteres seria ASCII ou uma variante. FAT é sigla pra tabela de alocação de arquivos. Assim como expliquei sobre MBR ou GPT dois episódios atrás, FAT é uma estrutura de dados que define como que arquivos e diretórios são gravados e organizados. O MBR ocupa o primeiro setor da primeira trilha do primeiro cilindro, logo na sequência os próximos setores vão guardar a FAT.</p>
<p>Naquela época a maioria das pessoas não tinha HDs, no máximo floppy ou disquetes. Acho que os primeiros tipos, que gravava só de um lado não era mais que 160 kilobytes no total. A gente grava informações sobre o disco e a tabela de arquivos, ou mais corretamente, tabela de alocação de arquivos que é File Allocation Table ou FAT, nos primeiros setores da primeira trilha do disco, lembra? Se as estruturas de dados pra guardar esses dados fosse muito grande, ia usar parte significativa desses 160 kilobytes.</p>
<p>Pra ter uma imagem na cabeça pense no seu disco sendo um linguição de bytes, que a gente pode desenhar como sendo várias linhas uma embaixo da outra. Você já sabe que HDs lêem bytes em blocos. O HD reconhece setores e vai lendo de 512 em 512 bytes. O primeiro setor do disco, lá no endereço 0 é o master boot record, que é uma estrutura de dados que vai configurar as partições. Sendo 512 bytes vai do endereço 0 até 01FF.</p>
<p>Existem várias versões de MBR, que foi evoluindo até ser substituido por GPT como você já sabe. Mas na primeira versão, a partir do endereço 01BE tinha entradas de partição, que é outra estrutura de 16 bytes. No MBR original eram 4 espaços de 16 bytes reservados pras 4 partições possíveis. Era só isso até criarem o conceito de partições extendidas, mas nem vou perder tempo detalhando porque hoje a gente usa GPT, que permite muito mais partições, mas também ocupa mais que um setor no começo do disco.</p>
<p>Independente se é MBR ou GPT, só entender que o primeiro setor do disco é reservado pra ter esse índice. E cada entrada de partição que você configurar com um fdisk da vida vai ter a coordenada em CHS no caso de MBR ou LBA no caso de GPT. Enfim, o endereço onde começa e outro endereço pra dizer onde termina. Daí a BIOS ou UEFI, depois de achar a primeira partição, vai continuar o bootstrapping, carregando o bootloader do sistema operacional, que finalmente vai carregar a kernel, drivers e terminar de bootar a máquina.</p>
<p>Agora você entende quando a gente abre um aplicativo como o Disk Management do Windows e vê essas partições. Elas ficam gravadas nessa área de GPT, e olha só como tem uma partição de 184 mega que é o EFI System Partition, que é pra onde a UEFI vai apontar no boot pra carregar a kernel do Windows. Tem outras partições maiores, de quase 500 mega, que provavelmente é a partição de recuperação. Quando você tá com problemas no boot, vai ter um sistema de emergência pra diagnosticar. Hoje que temos espaço sobrando, deixar reservado meio giga ou um giga, não é uma perda grande se for facilitar nossa vida caso algo dê pau.</p>
<p>Hoje é comum ter pelo menos 128 giga num HD barato. Mas no começo dos anos 80 o comum era no máximo ter 40 kilobytes num cartucho ou 160 kilobytes num disquete. A gente não tinha espaço pra ficar perdendo. Então imagine que os primeiros sistemas de arquivo tinham que ser super simples. Daria pra fazer vários episódios falando de cada um mas vou resumir.</p>
<p>O mais comum no começo foi o FAT12, que tinha endereços de 12 bits. Diferente do que fizemos com memória onde cada endereço aponta pra um byte na memória, endereços de HDs apontam pra clusters. Neste ponto vocês já devem estar começando a se acostumar com o conceito de uma estrutura de dados, tipo uma struct de C mesmo, onde cada campo tem um tamanho fixo em bytes. Sabendo o tamanho de cada campo podemos ir achando os campos no linguição de bytes. Foi assim quando expliquei sobre arquivos executáveis como ELF de Linux, ou o próprio MBR que é tipo uma estrutura de cabeçalho que define o drive.</p>
<p>Do byte 0 até 2 vai ter uma instrução de JUMP de assembly pra um endereço. O bytecode dessa instrução e o endereço vai depender da versão do MS-DOS ou Windows NT, mas é aqui que a BIOS passa o controle pro sistema operacional. Nos bytes seguintes vai ter outras informações como o nome do fabricante, daí vem as informações do layout do drive.</p>
<p>Nos bytes 11 e 12 tem o número de bytes por setor, que costuma ser 512 mas pode ir até 4 kilobytes que é 4096. No byte 13 tem o número de setores por cluster. Eu falei que o sistema de arquivos vai lidar com clusters e clusters pode ser um ou mais setores. É aqui que vai definir isso. Podemos colocar quanto de espaço queremos ter no drive.</p>
<p>Aqui que vem o 12-bits no FAT12. A tabela FAT12 consegue indexar até 2 elevado a 12 endereços pra clusters. Até 4 mil e noventa e seis clusters. Se o cluster tiver 2 setores de 512 bytes cada, o tamanho máximo que dá pra ter na partição vai ser 4 mil e noventa e seis vezes 2 setores, vezes 512 bytes, ou 4 megabytes. O que seria muito pouco. Mas se aumentar o tamanho dos setores de 512 bytes pra 4 kilobytes, esse valor aumenta pra até 16 megabytes.</p>
<p>Qual o problema de fazer isso? Lembra que expliquei no episódio de HDs que um arquivo vai ser gravado um setor de cada vez. Na realidade, vai ser gravado cluster a cluster porque é isso que o FAT reconhece. Então se cada cluster tiver 2 setores e cada setor tiver 4 kilobytes, se pegarmos um arquivo de 40 kilobytes pra gravar, precisamos reservar 2 clusters inteiros na FAT pros primeiros 32 kilobytes. Mas os 8 kilobytes restantes vai ocupar o próximo cluster inteiro. Ou seja, vai desperdiçar espaço no fim desse cluster. O arquivo de 40 kilobytes na realidade vai ocupar 48 kilobytes reais no disco. É um desperdício de 20% do disco.</p>
<p>Se você escolher um setor de 512 bytes como antes, cada cluster só vai ter 1 kilobyte. Mas o FAT só consegue indexar os mesmos 12-bits, 4 mil e noventa e seis clusters. Aí só dá pra ter uma partição de 4 megabytes em vez de 16. Naquela época isso não era ruim, porque a gente lidava mais com disquetes. No começo dos anos 80 o mais comum acho que era o disquete de 5 e 1/4 polegadas de 360 kilobytes. Então setores de 512 bytes tava de boa.</p>
<p>O problema foi do meio pro fim dos anos 80, quando passamos a ter HDs ou winchesters. Em 1990 eu tinha um winchester de 10 megabytes. Agora o FAT12 sozinho não ia dar conta. Por isso foi atualizado pra FAT16. Agora a tabela podia ter 2 elevado a 16, mais de 65 mil clusters. Se manter 2 setores por clusters e setores de 512 bytes mesmo, ainda assim já daria pra ter uma partição de até 64 megabytes. Então tava seguro. Até o meio dos anos 90 pouca gente tinha um HD tão grande assim.</p>
<p>Se você não tá acostumado com megabytes. Seu HD no seu computador aí deve ser de 128 giga ou mais. Se for um computador bem barato talvez seja de 64 gigas. Espero que não, porque o diretório C:\Windows sozinho  deve estar ocupando mais de 20 gigas. 1 giga é mil e vinte quatro vezes 1 mega. Ou seja, um HD de 64 giga é mil vezes maior que um HD de quase 30 anos atrás.</p>
<p>Mas aí, no meio dos anos 90, chegou a hora de aparecer o Windows 95, que ia tentar tirar o atraso nas interfaces gráficas que sistemas como Amiga e Mac já tinham faz anos. Mas construir uma interface gráfica em cima do FAT16 tinha um grande problema: a estrutura de dados que se chama &ldquo;directory entry&rdquo; era gravado assim no disco:</p>
<p>Os primeiros 11 bytes era o nome do arquivo de 8 bytes e uma extensão de 3 bytes. Só quem usou micros antes de 95 lembra disso. Era horroroso ficar criando vários documentos, planilhas, código fonte de programa e estar limitado a nomear arquivos só com 8 letras. A gente tinha que ser criativo. Mas era assim porque espaço de memória ou disco até aquele ponto custava muito caro. Lembra que eu falei que nome de arquivo ocupa espaço?</p>
<p>Eu lembro quando programava Clipper que a gente seguia essa convenção no nome dos campos em tabela de banco de dados. Era muito comum ter campos como CODCLI pra código de cliente, ou NOMCLI pra nome de cliente, DATVAL pra data de validade e assim por diante. Aliás, era pra economizar memória que datas eram gravadas no formato YYMMDD que é dois digitos pra dia, pra mês e pra ano, considerando que se YY fosse 80 então era 1980.</p>
<p>De qualquer forma, precisava resolver isso. Nessa entrada de diretório, que usava pra diretório e pra arquivo, o byte 11 depois da extensão era um vetor de bits. Outro dia vou falar das vantagens de um bitvector e bitmasks, mas deixa eu resumir.</p>
<p>Um byte tem 8 bits. Você pode pensar em cada bit como se fosse um interruptor de luz. 1 é ligado e 0 é desligado. E o campo inicializa com tudo zerado. No caso do FAT, se eu ligar o 1o bit, o bit 0, significa que é um arquivo ou diretório só de leitura. Se ligar o bit 1, é um arquivo ou diretório escondido. No caso de um Linux, ser escondido é ter o primeiro byte do nome sendo um ponto, os dotfiles. Mas no FAT é só 1 bit, então pra indicar se um arquivo é escondido em FAT usamos 7 bits a menos que no Linux.</p>
<p>Continuando, se ligar o bit 2 vai indicar que é um arquivo especial do sistema. Se ligar o bit 3 é a etiqueta ou nome do volume. Se ligar o bit 4 finalmente indica que essa estrutura tá definindo um subdiretório. Por outro, lado se ligar o bit 5 é um arquivo. Imagino que os bits 4 e 5, só um deles pode ser 1 e o outro 0. Já os bits 6 e 7 não significam nada e são ignorados.</p>
<p>Ou seja, em um único byte a gente conseguiu enfiar 6 informações diferentes. Se você fizesse do jeito ingênuo como uma struct com 6 campos int, ia gastar 6 bytes inteiros, 5 bytes a mais do que usando um único campo de 1 byte. Isso é muito comum pra guardar informações que é só verdadeiro ou falso, 1 ou 0. Você só precisa de 1 bit pra representar essa informação. Essa técnica aumenta a densidade de informações por byte.</p>
<p>E eu me dei ao trabalho de explicar isso porque a forma de permitir nomes longos a partir do Windows 95, foi atualizar o FAT16 pra VFAT. E a principal diferença é que toda vez que cria um arquivo vai criar duas entradas de diretório. Um igualzinho o que acabei de explicar, começando com 11 bytes pro nome no formato antigo de 8 letras e extensão de 3 letras. Mas a segunda estrutura ia entuchar mais bytes no meio pra caracteres, agora em formato UNICODE em vez de ASCII, já que a ambição do Windows 95 era expandir pelo mundo em diversas línguas.</p>
<p>Pra programas de DOS antigos não se confundirem, ainda ia ter a estrutura antiga, que ia ignorar a estrutura nova. Pensa que na mesma lista você tem as duas estruturas. Mas aí o DOS antigo ia ler o byte 11 que é o bitvector de atributos e encontrar 0xF, tudo ligado 11111111, que é um atributo inválido, então um DOS antigo ia ignorar.</p>
<p>Na prática, você podia digitar um documento no Word do Windows 95 e salvar como hoje, com um nome como &ldquo;Escopo do Projeto Secreto.doc&rdquo;. Vai gerar uma entrada de diretório com caracteres UNICODE normalmente e junto gerar uma segunda entrada com uma versão no formato antigo de 11 bytes. Provavelmente vai ser &ldquo;ESCOPO<del>1.DOC&rdquo;. Se gravar outro arquivo como &ldquo;Escopo do Projeto do Ano que vem.doc&rdquo;, vai gerar uma versão de DOS chamado &ldquo;ESCOPO</del>2.DOC&rdquo;.</p>
<p>Esse esquema dura até hoje no NTFS do Windows 10. Se não acredita abre um command prompt e digita &ldquo;cd c:\progra<del>1&rdquo; e ele cai no &ldquo;Program Files&rdquo;. Se abrir o Explorer do Windows e digitar &ldquo;C:\progra</del>1&rdquo;, ele vai te mandar certinho pro diretório &ldquo;Program Files&rdquo;. O DNA do DOS ainda existe.</p>
<p>O VFAT foi uma versão feita pro lançamento do Windows 95, mas logo no segundo DLC que foi o Windows 95 OSR2, a versão que funcionava de verdade, o VFAT foi substituído pelo FAT32. Você já imagina que a tabela subiu pra endereçar 2 elevado a 32 cluster em vez de 16, até 4 milhões de clusters. Com setores de 512 bytes isso daria uma partição de tamanho máximo teórico de 4 terabytes.</p>
<p>Se o cluster só tiver 1 setor, então ia até 8 terabytes, mas cada arquivo dentro só poderia ter 4 gigabytes. É 32-bits, então só dá pra ler e gravar 2 elevado a 32 bytes de dados crús, o que dá o famoso limite de 4 giga por arquivo. Mas a gente já viu no outro episódio que era a BIOS e o MBR que não permitiam ter partições maiores que 2 terabytes. Finalmente o FAT32 seria o filesystem que ia durar a virada do século.</p>
<p>Em paralelo a isso a Microsoft tentou muitas coisas dos anos 80 até começo dos 90 pra abrir mais pro mercado de workstations, onde tava a Silicon Graphics ou a Sun. Mas MS-DOS e Windows 3 não era suficiente. Por isso teve as iniciativas como o XENIX que foi um UNIX da Microsoft que não vingou e a parceria com a IBM que deu origem ao OS/2. Tem muito sistema como máquinas antigas de sacar dinheiro, as ATMs, sistema de bilhete de metrô e coisas assim que ainda rodam em OS/2.</p>
<p>Um dia eu tento falar do OS/2 mas ele era mais sofisticado que o Windows 95, que não era muito mais que um sistema parasita que espera o MS-DOS bootar em modo real pra depois ligar o modo protegido e dar controle pro Windows. Era uma gambiarra de coisas antigas de 16-bits misturado com coisas novas de 32-bits. A série do Windows 95, 98 até o famigerado Millenium Edition no final dos anos 90 foi uma bela de uma zona.</p>
<p>Já o OS/2 foi projetado pra ser um sistema mais completo de 32-bits. Acho que a primeira versão era 16-bits mas mudaram rápido. Eu cheguei a ver as primeiras versões do OS/2 que tinha a cara do Windows 3.1. O OS/2 mais famoso acho que o foi a versão 3 Warp e a que perdura até hoje em muitos lugares é a versão 4. E existe uma versão open source que foi feita via engenharia reversa, porque a IBM nunca abriu o código fonte do OS/2.</p>
<p>É quase impossível rodar um MS-DOS ou OS/2 dos anos 90 em máquinas modernas sem modificação. Pra começar, o processo de boot mudou de BIOS e MBR pra UEFI e GPT além disso o hardware de hoje tem mais integrações como ACPI e AHCI que é o Advanced Configuration and Power Interface e Advanced Host Controller Interface. O primeiro pra facilitar o sistema operacional encontrar hardware, configurar e gerenciar energia e a segunda pra falar com os controladores de SATA que fala com drives.</p>
<p>Seu HD provavelmente é SATA e não mais IDE ou SCSI. Sem entender AHCI, a gente nem encontra o HD. Quando você configura uma máquina virtual num VMWare ou Virtualbox da vida, vai notar que eles configuram um HD SCSI e não SATA. SCSI era a melhor interface da época, usado em workstations e Macs. PCs mais baratos usavam IDE mesmo.</p>
<p>Antigamente a gente tinha porta serial, porta paralela e barramento IDE que falava ATA. Ligava impressora na porta paralela, modem na serial. Era um saco configurar alguma coisa nessas portas e barramentos antigos. Ainda não tinha protocolos pros dispositivos se identificarem automaticamente pro sistema operacional. Pensa que isso exige mais bytes e um protocolo mais gordo e a velocidade de transferência era bem lenta.</p>
<p>Hoje em dia a gente tem conectores USB como Type C em vez do antigo SCSI, protocolos como USB 3.2 Gen 2 ou Thunderbolt 3 em vez do antigo ATA. Aliás, depois do ATA veio o Serial ATA ou SATA. E esses conectores se ligam em barramentos internos que falam direto com a CPU como PCI Express ou PCIe, que substituiu o antigo PCI e AGP. Essa é uma parte da história que tenho zero saudades: o tanto de problema que era conectar e configurar dispositivo. Nunca funcionava de primeira. Muito diferente de conectar um HD externo num Thunderbolt hoje, que é plug and play de verdade.</p>
<p>Mas vamos deixar dispositivos de lado por hoje. A parte importante é que da parceria da Microsoft com a IBM, no OS/2, surgiu o sistema de arquivos HPFS ou High Performance File System. E ele serviu de inspiração pra outro filesystem, o New Technology File System ou NTFS do Windows NT.</p>
<p>Em paralelo a essa história de FAT. No mundo dos Macs, tinha o Macintosh com o Macintosh File System, que era um pouco melhor que um FAT12 da vida. Mas com a Apple querendo lançar HDs nos Macintoshs eles evoluíram pra uma nova versão de filesystem chamada Hierarchical File System ou HFS.</p>
<p>Assim como o HPFS da IBM pro OS/2 e diferente do FAT, o HFS seria implementado usando B+Trees. Você já entendeu um pouco sobre estruturas de árvores alguns episódios atrás. E eu expliquei rapidamente sobre Balanced Trees ou B-Trees e derivações como B+Trees e B* Trees. Vocês vão ver que essas estruturas estão por baixo de quase tudo que você usa num computador.</p>
<p>Em resumo, são árvores balanceadas de baixa profundidado onde cada nó tem vários filhos de uma só vez. Além disso os filhos nos galhos internos só tem metadados e chaves, onde ficam os inodes. E só as folhas da árvore, os últimos nós externos lá embaixo, é que apontam pros dados em si. No caso, pra clusters ou blocos.</p>
<p>Tem a vantagem de ter tempo de procura razoavelmente bom, especialmente se comparar com uma mera tabela. E diferente de uma árvore binária, a profundidade vai ser baixa porque cada nó vai ter vários filhos em sequência. E como tem vários filhos por nó, nem toda operação de inserção ou deleção vai precisar promover um rebalanceamento, como eu mostrei que acontece numa árvore como red black trees.</p>
<p>Em discos, a vantagem é que isso ajuda a diminuir a fragmentação e aumenta a performance. Os dados são lidos e escritos em conjuntos grandes de blocos de uma só vez. Provavelmente vai desperdiçar um pouco de espaço. Mas isso é outro trade off, a gente troca um pouco de espaço em disco pra evitar fragmentação e aumentar a performance de leitura e escrita. Provavelmente tirando o antigo FAT, todos os file systems usam B+Trees. Mesmo o HFS da Apple em 1985 já era implementado com B+Tree.</p>
<p>Num Mac você sabe que dá pra abrir as propriedades de um arquivo e atribuir uma cor pra aparecer destacado no Finder. E dá pra selecionar o ícone, copiar, e colar em outro arquivo. Sem ter um arquivo de icone separado. E muito mais, mas você já pensou onde que se armazena essas coisas?</p>
<p>A cor do arquivo até pode ir nos metadados no nó da árvore mas e um ícone que tem múltiplos bytes? Ele vai num resource fork. Na realidade o nó da árvore pode apontar pra dois galhos de dados, um é o data fork que aponta pro primeiro byte do conteúdo do arquivo propriamente dito. O outro galho é o resource fork que aponta pro primeiro byte de um outro conjunto de dados escondido.</p>
<p>Por isso que quando você faz um arquivo zip comprimido num Mac e descompata num PC, vem um diretório estranho chamado &ldquo;underline&rdquo; MACOSX. Digamos que você tava compactando um arquivo chamado &ldquo;retrato.jpg&rdquo; então nesse diretório estranho vai ter um arquivo &ldquo;._ retrato.jpg&rdquo; que são os bytes que ficavam no resource fork.</p>
<p>Como nem todo filesystem tem suporte a resource forks, em particular FAT não tem nada nem perto disso, então o Mac é obrigado a criar arquivos de suporte pra não descartar informações. Se você copia de um Mac pra outro Mac, isso é transparente. Mas se usa um filesystem intermediário, ou um formato de arquivos como Zip, ele é obrigado a dar um jeito. O NTFS tem suporte a forks, que chama de ADS ou alternate data streams.</p>
<p>Se você abrir um command prompt num Windows 10 de hoje, sabe que dá pra criar arquivos texto como num shell de Linux. Basta fazer <code>echo &quot;hello world&quot; &gt; hello.txt</code> e tá criado. Se usar o comando <code>more hello.txt</code>  vai listar de volta o conteúdo. Trivial, todo mundo sabe isso. Mas o que muitos de você não sabem é que posso escrever bytes nesse alternate data stream do arquivo. Tente fazer <code>echo &quot;isto é um segredo&quot; &gt; hello.txt:segredo</code>. Pronto. Se usar o comando <code>more</code> de novo, não vai enxergar nada, só o texto original. O que aconteceu?</p>
<p>Agora posso fazer <code>more &lt; hello.txt:segredo</code> e olha só, o texto secreto apareceu! Se abrir num editor de textos normal, nunca vai ver esses bytes. Praticamente ninguém suporta alternate streams nos programas. Mas esse arquivo agora carrega esse segredo escondido. Pena que é um recurso que acaba sendo desperdiçado. Esse &lsquo;segredo&rsquo; é o nome do stream de bytes e a gente pode criar vários streams diferentes.</p>
<p>E é mais chato ainda porque se a gente tentar copiar esse arquivo pra algum pendrive em FAT32 da vida, o Explorer vai avisar que o arquivo tem propriedades que não vão ser copiadas e parar por aí. Daí perdemos aquele segredo porque, diferente do Mac, ele nem tenta criar um arquivo separado. Como as ferramentas que gerenciam arquivos do próprio Windows male male reconhecem esses forks, eles acabam sendo inúteis pra gente.</p>
<p>E por hoje vou pausar essa história porque no próximo episódio vou começar a falar sobre os outros filesystems e a nova geração que usamos hoje em dia e como esse estudo em estruturas de dados foi modificando a forma como pensamos em arquivos. Se ficaram com dúvida mandem nos comentários abaixo, assinem o canal e não deixem de clicar no sininho pra não perder a continuação. Compartilhem o video pra ajudar o canal. A gente se vê, até mais.</p>
]]></content:encoded><category>armazenamento-e-backup</category><category>algoritmos-e-estruturas-de-dados</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #97 - Só Precisamos de 640 kB de Memória? | 16-bits até 64-bits!</title><link>https://akitaonrails.github.io/2021/05/13/akitando-97-so-precisamos-de-640-kb-de-memoria-16-bits-ate-64-bits/</link><guid isPermaLink="true">https://akitaonrails.github.io/2021/05/13/akitando-97-so-precisamos-de-640-kb-de-memoria-16-bits-ate-64-bits/</guid><pubDate>Thu, 13 May 2021 10:41:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/cTVXKfYOYxo"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Um dia nos disseram que 640 kilobytes de RAM é o suficiente pra tudo que a gente precisasse. Isso foi no começo dos anos 80. Hoje quero mostrar como a gente lidava com essa limitação no MS-DOS, como era na geração anterior e como ficou na transição pra 32-bits, depois pra 64-bits, e porque isso é relevante pra você, como programador.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/cTVXKfYOYxo"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um dia nos disseram que 640 kilobytes de RAM é o suficiente pra tudo que a gente precisasse. Isso foi no começo dos anos 80. Hoje quero mostrar como a gente lidava com essa limitação no MS-DOS, como era na geração anterior e como ficou na transição pra 32-bits, depois pra 64-bits, e porque isso é relevante pra você, como programador.</p>
<p>Hoje vamos ter História!</p>
<p>Conteúdo:</p>
<ul>
<li>00:00 - Intro</li>
<li>01:25 - Recapitulando Mappers de 6502</li>
<li>08:48 - MS-DOS</li>
<li>12:13 - Segmento:Offset = 20 bits</li>
<li>18:50 - HMA, UMB, EMS, XMS</li>
<li>23:31 - Config.sys</li>
<li>27:51 - modo protegido e virtualização</li>
<li>31:26 - AWE/PAE</li>
<li>32:17 - swap/page faults</li>
<li>38:42 - 64-bits</li>
<li>41:26 - NX Bit/DEP</li>
<li>47:25 - Conclusão</li>
</ul>
<p>Links:</p>
<ul>
<li>Tutorial of ARM Stack Overflow Exploit – Defeating ASLR with ret2plt (<a href="https://www.fortinet.com/blog/threat-research/tutorial-of-arm-stack-overflow-exploit-defeating-aslr-with-ret2plt"target="_blank" rel="noopener">https://www.fortinet.com/blog/threat-research/tutorial-of-arm-stack-overflow-exploit-defeating-aslr-with-ret2plt</a>)</li>
<li>How do ASLR and DEP work? (<a href="https://security.stackexchange.com/questions/18556/how-do-aslr-and-dep-work"target="_blank" rel="noopener">https://security.stackexchange.com/questions/18556/how-do-aslr-and-dep-work</a>)</li>
<li>Memory Prices 1957+ (<a href="https://jcmit.net/memoryprice.htm"target="_blank" rel="noopener">https://jcmit.net/memoryprice.htm</a>)</li>
<li>Mastering x86 Memory Segmentation (<a href="https://www.eejournal.com/article/mastering-x86-memory-segmentation/"target="_blank" rel="noopener">https://www.eejournal.com/article/mastering-x86-memory-segmentation/</a>)</li>
<li>Configuring your MS-DOS properly (<a href="https://www.cubic.org/docs/configuring.htm"target="_blank" rel="noopener">https://www.cubic.org/docs/configuring.htm</a>)</li>
<li>Autoexec.bat and config.sys information (<a href="https://www.computerhope.com/ac.htm"target="_blank" rel="noopener">https://www.computerhope.com/ac.htm</a>)</li>
<li>4DOS Introduction and Installation Guide (<a href="https://wiki.preterhuman.net/4DOS_Introduction_and_Installation_Guide"target="_blank" rel="noopener">https://wiki.preterhuman.net/4DOS_Introduction_and_Installation_Guide</a>)</li>
<li>The  GRUB  MBR (<a href="https://thestarman.pcministry.com/asm/mbr/GRUB.htm"target="_blank" rel="noopener">https://thestarman.pcministry.com/asm/mbr/GRUB.htm</a>)</li>
<li>NES EMULATION (<a href="https://people.ece.cornell.edu/land/courses/ece4760/FinalProjects/s2009/bhp7_teg25/bhp7_teg25/index.html"target="_blank" rel="noopener">https://people.ece.cornell.edu/land/courses/ece4760/FinalProjects/s2009/bhp7_teg25/bhp7_teg25/index.html</a>)</li>
<li>6502 Instruction Set (<a href="https://www.masswerk.at/6502/6502_instruction_set.html"target="_blank" rel="noopener">https://www.masswerk.at/6502/6502_instruction_set.html</a>)</li>
<li>ROMs and Mappers: Why NES Games Can Be So Different On The Same Hardware (<a href="https://abad1dea.tumblr.com/post/187429390285/roms-and-mappers-why-nes-games-can-be-so"target="_blank" rel="noopener">https://abad1dea.tumblr.com/post/187429390285/roms-and-mappers-why-nes-games-can-be-so</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Hoje vai ser um video bem puxado. O pior é que metade dos detalhes técnicos que vou explicar, na prática você nunca precisaria saber, mas quero explicar mesmo assim. Minha idéia é continuar um pouco aqueles vídeos de introdução à computação e videogames onde comecei a explicar sobre a arquitetura de 8-bits de processadores como o 6502 da nintendinho e acelerar em direção a como era nos PCs na era dos 16-bits e o que significa migrar pra 32-bits e depois pra 64-bits do ponto de vista de escovação de bits.</p>
<p>Se alguém tentou programar um pouco no fim dos anos 80, deve se lembrar da barreira dos 640 kilobytes de RAM. Boa parte de hoje vai ser explicar sobre o que era isso e como a gente fazia. Quem já começou hoje, na era de 64-bits, não sabe a sorte que tem de ter endereços sobrando de memória. Pra vocês vai ser bom pra entender porque programador velho fica ranzinza falando que programar hoje é bem mais fácil que antigamente. Vamos entender de onde vem isso.</p>
<p>Como eu falei antes, metade das técnicas que vou falar hoje e metade dos termos que vou falar são legados. Pense mais como curiosidade de museu do que alguma coisa que você deveria saber de cabeça. Mas acho que a noção de como as coisas funcionavam pode dar insights de como você enxerga programação hoje e como as coisas podem evoluir a partir daqui. Então vamos lá.</p>
<p>(&hellip;)</p>
<p>Vamos voltar um pouco atrás nos episódios. Vocês vão se lembrar que falamos rapidamente sobre os videogames de 8-bits, que usavam processadores 6502 ou derivados e como eles carregam dados de cartuchos, que são basicamente chips de memória ROM, com um certo range, ou intervalo de endereços. Toda vez que um jogo precisa de mais de 40 kilobytes de memória, o cartucho pode trazer um chip controlador, um mapper.</p>
<p>Eu expliquei isso no episódio sobre o NES, nosso Nintendinho, pra mais detalhes reveja o video no link acima. Mas basta lembrar que o 1o Super Mario é provavelmente um dos jogos mais complicados que se podia fazer dentro dos limites de 40 kilobytes de um cartucho sem mapper, onde temos uns 32 kilobytes pro código do jogo e míseros 8 kilobytes pros gráficos. Por isso as fases do primeiro Super Mario reusam os mesmos gráficos, os tiles e sprites, da primeira fase, mas só mudando a paleta de cores. Os inimigos são sempre os mesmos. E por aí vai.</p>
<p>Se a gente abrir o binário do jogo num emulador como o Mesen e olharmos a região chamada CHR do cartucho, aqui estão todos os gráficos do jogo inteiro. É só isso que tem no primeiro Super Mario. Pra conseguir caber tudo nesse espaço, os programadores tiveram que ser muito criativos. Exemplo famoso é ver esta tela da primeira fase. As nuvens e os arbustos usam os mesmos tiles, só muda a paleta de cores, tão vendo?</p>
<p>Lembram como nos episódios anteriores eu falei que o sistema operacional costuma dividir endereços de memória, segmentar regiões como Heap, Stack e outros? Mesma coisa pro 6502, em particular no NES. Por exemplo, a stack de um NES vai do endereço hexa 01A0 até 01FF, que dá 95 bytes. Aliás, toda vez que tiver dúvidas, abre uma calculadora como essa que vem no Windows em modo programador e faz a conta em hexadecimal, 1FF menos 1A0 e vai dar 5F que convertendo em decimal é 95. Apesar de parecer pouco, todo jogo de nintendinho, até os mais complexos, funcionam nesse espaço.</p>
<p>O código do programa em si fica entre os endereços 8000 a FFFF, divididos em bancos. Os bancos de programa costumam ser chamados de PRG. Se a gente olhar no emulador Mesen, dá pra puxar o que tem na ROM PRG. No visualizador ele começa do endereço 0 e vai até 7FFF. Quer dizer que é o endereço 0 desse banco, dentro desse segmento da ROM PRG. Mas nos endereços globais em vez de 0 é 8000.</p>
<p>De curiosidade, no cartucho do Super Mario o linguição de bytes começa com 78 depois D8 A9 10 8D 00 20 A2 FF e assim por diante. Podemos pegar qualquer tabela de conjunto de instruções de 6502, como esse aqui  pra converter em mnemônicos de Assembler. Daí podemos ver que 78 é a instrução SEI ou Set Interrupt Disable. Ele não tem parâmetros, então o próximo D8 é outra instrução, CLD que é Clear Decimal, também sem parâmetro.</p>
<p>Agora A9 é LDA, que a gente já tinha visto nos outros vídeos. É o Load Accumulator. Ele vai pegar o endereço seguinte, que é FF e carregar o que tiver lá no acumulador. Depois 8D que é a instrução STA ou Store Accumulator que vai pegar o que tiver no acumulador e mandar pro endereço 20 00. Como o 6502 é little endian, ele grava o low byte primeiro, então inverte e a gente lê como 00 20. Esse endereço é da PPU, que é o chip que controla os gráficos.</p>
<p>Em vez de ficar traduzindo assim byte a byte na unha, melhor usar um recurso do emulador Mesen que é o debugger. Ele traduz tudo pra gente. E olha só, veio exatamente o que acabei de falar a partir do endereço 8000. Assim como um binário ELF que é executável de Linux, um cartucho de NES começa com um cabeçalho de 16 bytes que vai dizer pro processador o que ele vai encontrar no cartucho. Por exemplo, os mappers que fazem trocas dos bancos de ROM pra ter mais espaço do que é possível endereçar.</p>
<p>Dependendo do tipo de cartucho, como do Super Mario que não tem mapper, que a gente chama de mapper zero, ele manda dar jump pro endereço 8000 que é onde tem a primeira instrução do jogo. Não dá pra endereçar mais de 40 kilobytes de ROM, mas com um mapper ele vai trocando os bancos de 16 ou 32 kilobytes dinamicamente. Dá pra ter quantos bancos você quiser. Eles respondem nos mesmos endereços, daí quando a gente pede pra trocar, os mesmos endereços passam a retornar dados diferentes. Quando a gente pede o endereço 8000 do banco 1 vai vir uma instrução. Se pedirmos o mesmo endereço 8000 mas do banco 2, vai vir uma instrução diferente.</p>
<p>Bancos são como se fossem páginas, e endereços são linhas nessas páginas. Um jogo que usa no máximo 32 kilobytes de ROM PRG e 8 kilobytes de ROM CHR é como se fosse uma página de papel, e cada endereço é uma linha. Temos linhas que vão de 0000 até 2000 que costuma ser os 8 kilobytes de CHR e do endereço 8000 até FFFF que costuma ser os 32 kilobytes de PRG.</p>
<p>Com mappers, o jogo tem mais páginas, como um livro. A gente pede a página pro mapper, mas o número de linhas continua o mesmo. Então, a primeira linha da página 1 vai ser diferente da primeira linha da página 2 e assim por diante. Fica mais complicado porque a gente tem que saber de que página pedir dados, mas a vantagem é que mesmo com um número de linhas limitado por página, dá pra ter várias páginas extras e assim podemos ter um Super Mario 3 com mais de 200 kilobytes e jogos ainda maiores como o Kirby Adventure que saiu quase no fim da vida do NES com quase meio megabyte e por isso os gráficos eram quase o que um Super Nintendo faria.</p>
<p>Isso tudo dito, vamos ver os endereços de 0 a 2000, com ou sem banco de mappers. É onde ficam os tiles, os gráficos. Um sprite é feito usando uma combinação de tiles diferentes. E podemos atribuir diferentes paletas de cores. Com isso podemos reusar esses tiles pra coisas diferentes. O exemplo mais citado são os tiles de nuvem, que usam a paleta que faz ficar branco. Se escolher a paleta verde, vira um arbusto. E se o design for feito de maneira criativa, dá pra economizar muito espaço de tiles reusando com cores diferentes.</p>
<p>O objetivo desse exercício todo de endereços, intervalos de endereços, estruturas fixas como tiles e sprites, é pra relembrar que no começo da programação a gente começa lidando com as formas mais simples de representar e mapear dados. Do ponto de vista de uma CPU de nintendinho, a única coisa que ele sabe é manipular memória de 0 até FFFF. No caso do processador 6502, vai reservar alguns desses endereços em conjuntos de segmentos. Vai ser hardcoded que no endereço hexa 4016 é que você vai ler quais botões do controle foram apertados, por exemplo.</p>
<p>Apesar de 0 até FFFF ter um total de 65 kilobytes de endereço, parte desses endereços é reservado pros 2 kilobytes de RAM, pros controles, pro video, pro audio e vai sobrar perto de 32 kilobytes pra programas e 8 kilobytes pra gráficos, por isso eu falei que os primeiros jogos eram limitados em 40 kilobytes. Pra ter mais espaço, precisa usar truques como os mappers. Mas na prática é como se a CPU tivesse acesso a um arrayzão de 0 até 65 mil 535 posições. E se assistiu meu episódio sobre os Hello World, você já sabe como funciona um array.</p>
<p>Antigamente, a quantidade de memória que a gente tinha era super limitada. Hoje temos gigabytes de memória, mas mesmo assim ainda parece pouco. O maior jogo de nintendinho não tinha meio megabyte. Literalmente qualquer foto que você tira num smartphone tem megabytes de tamanho. Por causa disso a gente consegue organizar esses recursos de uma forma mais ergonômica pra programadores. Um desses confortos é não precisar acessar as coisas diretamente em endereços e fazer cálculos de offset de endereços pra achar as coisas.</p>
<p>Num nintendinho, o total de endereços é fixo em 16-bits, sempre vai ser no máximo 64 kilobytes de endereços. Pra ter mais que isso precisa de um mapper, um controlador que vai mapear páginas. É mais ou menos isso que a gente fazia em MS-DOS nos PCs dos anos 80. Todo mundo deve ter ouvido a famosa frase atribuída ao Bill Gates que dizia que PCs não precisam de mais do que 640 kilobytes de memória. Era o máximo que o DOS conseguia endereçar na época.</p>
<p>Se você tava acostumado com micros de 8-bits com endereços de 16-bits, então o máximo que dava pra enxergar era 64 kilobytes. Em poucos anos ter 10 vezes mais memória parecia impressionante. Mas nós programadores sempre vamos dar um jeito de encher toda memória que tem. Toda vez que alguém disser “ah, o mundo só precisa de X gigabytes de RAM”, sempre vai aparecer um Chrome dizendo “hold my beer”.</p>
<p>No fim o MS-DOS também acabou adotando coisas similares a mappers de cartucho, mas em software. Vocês hoje em dia felizmente nunca vão ter que lidar com DOS, mas se um dia tiver curiosidade de usar um DOSBox ou FreeDOS seja pra rodar programas velhos ou pra games antigos mesmo, vai acabar esbarrando em alguns termos estranhos. O que eu vou explicar a seguir não vale mais pra PCs modernos, mas ter essa noção na cabeça pode ajudar a enxergar arquitetura de computadores de forma diferente, por isso acho que vale a pena. Não se preocupem em decorar números ou nomes que vou dizer, só tenta pegar a idéia geral.</p>
<p>O MS-DOS era um sistema operacional pra máquinas de 16-bits. Dessa geração pensar um Super Nintendo ou Mega Drive. Mas ele era compatível só com processadores tipo Intel 8086 e 8088 e inicialmente rodava no que passou a se chamar de modo real. Nesse modo os programas têm acesso a todos os endereços que era possível endereçar. Lembra que quando falamos em 8-bits ou 16-bits ou 32-bits em CPUs estamos falando do tamanho dos registradores.</p>
<p>Normalmente o barramento de endereços tem tamanho de bits diferentes. Como no caso do 6502 do NES: era uma CPU com registradores de 8-bits mas que acessava endereços de 16-bits pra ler ou escrever valores de 8-bits na RAM. Eu expliquei como isso funciona em detalhes no video do Guia mais Hardcore de Introdução à Computação. Só lembre que o tamanho do registrador não é diretamente ligado ao tamanho dos endereços.</p>
<p>Com as CPUs saltando de 8-bits pra 16-bits, a gente poderia imaginar que a próxima geração de endereços ia melhorar e saltar dos 16-bits direto pra 32-bits. Isso porque parece que estamos só dobrando os números, mas se lembrem que isso são potências de base 2. O salto não é o dobro, linear, e exponencial. 16-bits endereçava no máximo 64 kilobytes, que mesmo pra época era bem apertado. Mas 32-bits é 2 elevado a 32 que seria 4 gigabytes possíveis. No meio dos anos 80, meros 2 megabytes custavam 600 dólares. 4 gigabytes - se fosse possível comprar - custaria nada mais, nada menos que mais de um milhão e duzentos mil dólares. 2 megas!</p>
<p>Aumentar o barramento de endereços pra 32-bits significa ter 32 pinos, mais transistores e tudo mais, o custo do chip aumenta bastante. Aí provavelmente eles pensaram, “cara, a gente tem um processador com registradores de 16-bits. Pra que dobrar a quantidade de bits nessa geração, se vai levar décadas pra existir 4 gigabytes barato?”</p>
<p>64 kilobytes é realmente muito pouco. Se carregar uma planilha num Lotus 1-2-3 da vida, ia faltar memória bem rápido. Em vez disso vamos fazer uma gambiarra. Vamos criar novas instruções que peguem pares de registradores de 16-bits e vamos chamar um de segmento e outro de offset e com isso calcular um endereço de 20-bits. Em vez de desperdiçar fazendo um barramento de 32-bits, 20-bits é mais que suficiente.</p>
<p>Com 16-bits você só consegue enxergar até o endereço FFFF, quatro Fs que chega ao máximo de 64 kilobytes. Colocando só mais 4-bits de endereços, temos a capacidade de endereçar até 2 elevado a 20 que dá 1024 bytes ou 1 megabyte de endereços. É 16 vezes mais memória que a geração do Nintendinho. Esse é o poder da exponencial, não se esqueçam disso.</p>
<p>Vamos entender essa gambiarra, não seria um endereço linear de 0 até FFFFF, ou 5 Fs. Porque os registradores eram de 16-bits. Então precisava dar um jeito de usar dois valores de 16-bits pra calcular um endereço de 20-bits. Era horrível programar em baixo nível naquela época. Eu felizmente não precisei, mas tenho dó de quem usava Assembler de 8088.</p>
<p>Os endereços podiam ir de 0000:0000 até FFFF:FFFF, notem os dois pontos. Olhando assim parece um endereço de 32-bits. Mas são só 2 valores de 16-bits um atrás do outro. Na verdade você pega o primeiro segmento e multiplica por 16, hexa 10, que é o mesmo que fazer shift pra esquerda e colocar um zero. Daí você soma com o segundo segmento, que é um offset. É que nem um array, onde você tem o endereço do começo do primeiro elemento e vai somando com um offset pra cada posição. Offset é uma coisa que você vai sempre ver em computação, então vale estudar se ainda não entende.</p>
<p>Essa fórmula é arbitrária. Literalmente tiraram da bunda. Daí um endereço segmentado de FFFF:0010 é igual a FFFF * 16 bits ou 0x10 em hexa, que vai dar FFFF0 mais o offset 0010 que vai resultar no hexa 100000 que é o último endereço do limite de 1 megabyte quando você tem só 20 linhas de endereçamento.</p>
<p>Mas o maior endereço que dá pra representar concatenando dois valores de 16 bits é FFFF com FFFF. De novo, faz o primeiro FFFF shift pra esquerda que dá FFFF0 mais o offset FFFF que vai dar o endereço de 20-bits 10FFEF que é quase 64 kilobytes acima de 1 megabyte. Esses 64 kilobytes no topo dos endereços é conhecido como HMA ou High Memory Area, porque fica na parte mais alta dos endereços.</p>
<p>Nos processadores 8086 e 8088, quando o processador recebia uma requisição pra um endereço acima do 1 megabyte que é FFFF:0010, ele fazia um wrap, ou seja, truncava os primeiros bits mais da esquerda, então 100000 virava 00000, 100001 virava 00001 e assim por diante. É mais ou menos que nem fazer o módulo por 100000. Lembra que nos episódios anteriores falei que era importante ter noção de módulo.</p>
<p>Se a memória fosse uma fita de papel, com posições numeradas de 0 até 100000, é como se eu pegasse a ponta final dessa fita e grudasse com a ponta inicial 0 e aí viraria um anel. Quando chega no último endereço FFFFF, ele volta pro 0. Por isso nunca chega no endereço teórico máximo que falei agora, o 10FFEF porque truncando os primeiros 4 bits vira 0FFEF. Ele faz isso pra nunca tentar pegar um endereço alto inválido.</p>
<p>Mas os problemas não acabam aí. Se você parar dois segundos pra pensar nesse esquema de cálculo, vai ver que muitas combinações de segmentos e offsets acabam no mesmo endereço de 20bits. Sem pensar muito, peque o endereço 0001:0010 e 0002:0000. Faça a conta. No primeiro é 1 com shift pra esquerda que dá 10, mais 10 do offset vai dar 20. No segundo já é 2 shift pra esquerda que é 20 somado com 0 que é 20. Os dois vão chegar no endereço 00020. Tem mais de 4 mil combinações de segmento e offset que vão conflitar.</p>
<p>Isso é um problema porque você pode pensar que tá escrevendo num endereço e acaba sobrescrevendo em cima de outro errado sem saber. Parecem diferentes porque a notação de segmento e offset são diferentes mas ambos resultam no mesmo endereço real de 20 bits. Pra controlar um pouco a zona, virou uma convenção normalizar a notação. Então dentre essas duas opções que chegam no mesmo endereço, o correto seria escrever olhando a partir do endereço de 20-bits, enfiar os dois pontos entre os dois últimos hexa e chegamos em 0002:0000, que é considerado o endereço normalizado.</p>
<p>Pensa que zona, na hora de programar ter que ficar calculando essas conversões de cabeça. Óbvio que mais cedo ou mais tarde vai dar pau. Fico imaginando que é um pouco o que engenheiros nos Estados Unidos sofrem se precisam trabalhar em projetos que é em sistema métrico e imperial ao mesmo tempo. Uma hora escreve em polegadas, depois em centímetros, depois em jardas, daí faz temperaturas em Fahrenheit e converte pra Celsius e assim vai até uma hora explodir um foguete.</p>
<p>Independente disso, de fato a gente não precisava de 4 gigabytes de RAM no meio dos anos 80, mas claramente mesmo 1 megabyte sendo 16 vezes mais que os pífios 64 kilobytes de micros como Apple II ou Commodore 64 ou o nintendinho, aplicativos de produtividade como Lotus, Wordstar, dBase, nos fizeram passar a usar mais e mais memória e 640 kilobytes rapidamente começaram a se mostrar insuficientes.</p>
<p>Diferente de micros antigos e consoles de videogame, a gente não tinha a opção de cartuchos em PCs. Primeiro que seriam mais caros. E mesmo ainda sendo caro, era melhor usar RAM. Com disquetes ou HDs, você precisa carregar os bits do programa na RAM pra poder usar. Com cartucho não precisava carregar porque ele já é o próprio chip de memória. Quando pluga o cartucho o programa já tá carregado e pronto pra usar. Por isso que com poucos kilobytes de RAM era suficiente porque não ia ser gasto carregando os programas. Mas em PCs, parte da RAM vai ser sempre usado pra carregar o sistema operacional e os programas e o que sobrar é onde você vai trabalhar seus dados.</p>
<p>Entenda o problema. Independente se a gente colocar mais memória, todo programa, incluindo o sistema operacional tipo DOS e seus clones como DR-DOS, drivers de video, drivers de áudio, tudo carregava na memória convencional de 640 kilobytes. Sobrava bem pouca memória. E os programas ficavam cada vez maiores. Rapidamente 640 kilobytes não ia ser suficiente. Mas a gente não pode usar endereços acima de 640k, ou seja a partir de A0000 até FFFFF, porque são reservados. Do A0000 até CFFFF é memória de vídeo. De F0000 até FFFFF é memória pra BIOS. E como existia a regra de truncar acima do endereço 9FFFF pra voltar pra zero, mesmo plugando mais chips de memória, não ia ter como endereçar eles.</p>
<p>Por isso começaram a pensar em formas de quebrar essa limitação de 640 kilobytes. As primeiras soluções foi fazer algo parecido com os mappers de cartuchos de nintendinho: uma placa pra ligar nos slots ISA da placa-mãe com mais chips de RAM, talvez mais 1 megabyte, e um chip controlador como um mapper que seria carregado em algum lugar nos 640 kilobytes de RAM convencional. Daí você chamaria alguma interrupção ou função do sistema pra fazer bank switching, que é a mesma ideia que já vimos de como ter um cartucho de mais de 40 kilobytes.</p>
<p>O trecho reservado de 384 kilobytes no alto dos endereços acima dos 640 kilobytes convencionais foi chamado de upper memory area ou UMA. E uma das primeiras gambiarras foi usar endereços nesse espaço pra endereçar a memória extra, enfiado entre os endereços de periféricos e da BIOS. Soluções desse tipo, com um hardware especializado, criavam o que a gente chamava de EMS ou Expanded Memory ou memória expandida. Daí a gente podia ter muito mais de 1 megabyte, talvez 8 megabytes, algumas soluções iam até 32 megabytes. Isso no fim dos anos 80, com processadores 8088.</p>
<p>Se eu entendi direito, EMS usa um intervalo de endereços nessa região de memória alta reservada de 384 kilobytes, criando páginas pequenas, de 16 kilobytes, que é o EMS page frame. Daí seus dados poderiam ser carregados na memória acima de 1 megabyte e o driver de EMS vai fazendo page swaps de 16 kilobytes de cada vez. Bill Gates parece que odiava essa gambiarra. E era mesmo uma gambiarra, que se você entendeu como funciona bank switching de mappers de cartucho de nintendinho, é mais ou menos a mesma coisa. Quando saíram os 80386 aí não precisava mais de hardware especializado e o controle de EMS passou a ser emulado só via software.</p>
<p>Independente do EMS, a partir do 80286, você podia colocar mais pentes de memória e extender memória com endereços acima de 1 megabyte. Não dá pra carregar programas nesses endereços, mas você podia armazenar dados que um programa carregado na memória convencional podia usar. Tipo carregar o Lotus 1-2-3, que era o Excel da época, nos primeiros 640 kilobytes e as planilhas nos endereços acima de 1 megabyte, indo a um máximo de 15 megabytes.</p>
<p>Memória eXtendida é diferente da memória Expandida que falamos agora. Expandida é o EMS, que controla páginas de 16 kilobytes na memória alta e só funcionava com hardware especializado ou com os 80386. Mas nos 80286 passamos a poder acessar a memória acima de 1 megabyte. Pra isso precisava desligar o sistema de truncar endereços acima de 1 megabyte.</p>
<p>Resumindo até agora, a gente tinha memória convencional que são os primeiros 640 kilobytes de RAM onde carregava o DOS e seus programas. Daí tinha os últimos 384 kilobytes que era o UMA ou memória alta. Depois disso, antes do 80286, truncava os endereços. Acima da marca de 1 megabyte ainda tinha uma sobra de 64 kilobytes de endereços que se chama HMA ou high memory area. E com o 80286 agora temos a possibilidade de endereçar acima do 1 megabyte, a XMS ou memória extendida. No 286 ele chamou o esquema de ver memória até 1 mega e truncar como modo real, e pra acessar mais memória criou um conjunto de mecanismos e chamou de modo protegido. Você só podia estar em um desses modos de cada vez.</p>
<p>Se você achou complicado a idéia de endereços de memória de 20-bits divididos em segmentos de 16-bits, num 80286 em vez de endereços de 20-bits, eles aumentaram pra 24-bits, o que dava pra endereçar um máximo de 16 megabytes. Mas com esse esquema de segmentação, em modo protegido, cada processo recebia um segmento virtual separado de até 8 megabytes. Mesmo se você tivesse 16 megabytes, poderia rodar dois programas, cada um mapeando até 8 megabytes de cada vez.</p>
<p>Pra acessar a memória extendida, XMS, a gente escrevia um arquivo chamado Config.sys que  o DOS carregava logo que bootava. Precisava pelo menos carregar o driver HIMEM.sys que significa High Memory. Hai-mém e não Hímem! Com o HIMEM.sys e colocando a diretiva de DOS = HIGH, UMB, significa que ele ganhava acesso aos endereços da memória alta, o segmento entre 640K e 1 megabyte. UMB significa Upper Memory Block. Isso carregava o sistema operacional nos blocos sobrando nessa memória alta. Daí dava pra carregar alguma coisa lá, economizando a memória convencional, já era um pequeno ganho.</p>
<p>Como era pouco espaço de blocos de memória alta, a gente carregar um gerenciador de memória lá, como o EMM386 da própria Microsoft. Originalmente ele ia servir como software de emulação pra memória expandida, o EMS, fazendo page frame na memória alta pra paginar a XMS. Mas depois passamos a não precisar fazer mais isso, então era só passar o parâmetro NOEMS pra EMM386. Combinando a HIMEM com a EMM386 ganhamos acesso à memória alta e à XMS. Daí tinha diretivas como LH que acho que era Load High e DRIVERHIGH que é literalmente carregar drivers na High Memory Area, que são aqueles últimos 63 kilobytes de endereços depois do 1 megabyte, que processador antigo truncava o endereço.</p>
<p>Aliás, desde aquela época você vê que programador é bem pragmático pra criar nomes. Mas só no que expliquei até agora a gente tinha HIMEM e motorista bêbado com DRIVER HIGH. Vai vendo.</p>
<p>Raspando o tacho, pegando kilobyte livre onde dava, a gente economizava memória convencional, os primeiros 640 kilobytes onde roda programa, porque o sistema operacional DOS, e a maioria dos drivers como de placa de som, mouse, CD e tudo mais carregava ou na UMB ou na HMA e os programas conseguiam carregar dados na XMS. Economizar os 640 kilobytes era crucial pra carregar programas e jogos que cada vez ficavam maior. E na prática vira e mexe um programa não rodava mesmo porque faltava memória. Então essas otimizações todas não eram opcionais não.</p>
<p>E isso porque eu nem falei do CEMM da Compaq, do QEMM da Quarterdeck, do 386Max da Qualitas e outras soluções concorrentes pra gerenciar essa zona de memória. Entre o MS-DOS 3 até o MS-DOS 5 tinha várias combinações de gerenciadores de memória e alternativas ao MS-DOS como o concorrente DR-DOS da Digital Research. As coisas só ficaram menos ruins pra gente que era usuário quando saiu o DOS 6.2 que já vinha com todas essas ferramentas embutidas, como o EMM386, Memmaker, Scandisk e muito mais. E nesse meio tempo a Microsoft veio introduzindo o Windows 1.0, Windows 2.0.</p>
<p>Você pensa que daí não tinha como piorar, mas depois da introdução do modo protegido nos 80286, que era meio bugado, depois a introdução do modo Enhanced nos 80386, junto com Windows 2.0 pra 286 e Windows 3.0 pra 386, a coisa ficou mais confusa ainda antes de melhorar. Mas à medida que o Windows 3.1 com Enhanced Mode do Intel 386 começou a se popularizar, a necessidade de gerenciar memória na unha começou a diminuir. Com o 386 saltamos da geração de 16-bits pra 32-bits.</p>
<p>A importância de um Windows 95 foi dar um ponto final em uma década de bateção de cabeça da gente tentando extrair kilobytes da maldita limitação de 640 kilobytes de memória convencional, tentando entuchar o máximo possível nos kilobytes acima de 640 e ainda tendo que lidar com o esquema esdrúxulo que a Intel inventou de segmentação de endereços, fazendo aquele cálculo horroroso pra transformar dois registradores de 16-bits num endereço de 20 ou de 24 bits. Só que esse esquema se perpetuou nas APIs de 32-bits também.</p>
<p>Do ponto de vista do hardware, a gente começou no fim dos anos 70 com os processadores 8086 e 8088 com barramento de endereço de 20 pinos, ou 20-bits. Mas só tinha registradores de 16-bits. Em vez da Intel juntar dois registradores de 16-bits e concatenar pra fazer um endereço de 32-bits, fizeram o esquema de segmento e offset. Com offsets de 16-bits a gente tinha que lidar com blocos de 64 kilobytes de cada vez. E os endereços acima de 1 megabyte eram truncados pra dar a volta.</p>
<p>Daí no começo dos anos 80 sai o 80286 que introduz o modo real que é como rodava o 8086 com endereços de 20-bits. E o modo protegido dava opção de memória virtual e protegida, que é a idéia do sistema operacional alocar tabelas pra controlar o endereçamento dos programas. A grosso modo, em vez de todo programa enxergar os mesmos endereços reais, vão enxergar endereços virtuais de uma tabela local, exclusiva pra cada programa, que é controlado pelo sistema operacional, que vai ser o único que enxerga uma tabela global. Um programa que assumia ter acesso a endereços reais de hardware quebrava nesse modo.</p>
<p>Com esse controle de tabelas, a idéia é evitar que um programa escreva em cima do endereço do colega do lado, que é uma coisa que acontecia o tempo todo quando a gente tentava rodar mais de um programa ao mesmo tempo num MS-DOS da vida usando TSR que é como os programas ficam ativos em background. A CPU 80386 foi o divisor de águas e que marca a transição mesmo pro mundo 32-bits. Nela Intel colocou uma funcionalidade de Virtual 8086, que faz programas MS-DOS rodarem em ambiente isolados, onde um não enxerga a memória do outro.</p>
<p>Num Windows, quando você pede pra abrir programas de DOS ele abre o equivalente a uma máquina virtual. Virtualização, que é uma tecnologia da época dos mainframes, entrou no mundo de microcomputadores nessa época. E mesmo fora do Windows existiam alternativas de controladores de concorrência em modo texto como o ConcurrentDOS ou GEOS e mais, mas no final já sabemos que quem ganhou a concorrência foi o Windows mesmo.</p>
<p>Hoje em dia não temos mais esse subssistema mas do Windows 3 até o Windows NT, isso era importante porque mesmo indo pra interface gráfica, a maioria dos programas mais importantes rodava em modo texto no DOS. Só a possibilidade de rodar mais de um programa de DOS concorrentemente, sem um crashear o outro, já era um bom motivo pra migrar pra Windows. O outro motivo era a uniformização das APIs pra programadores. Antigamente, tinha uma dúzia de empresas só fazendo gerenciadores de memória. Sem falar em outras coisas como drivers de áudio, drivers de video. Pra áudio precisava programar pra Sound Blaster, Adlib ou Roland. Pra video precisava manualmente tomar conta de modo CGA ou EGA ou VGA. Com Windows era uma API só pra tudo e o Windows lidava com hardware por trás.</p>
<p>Na era 32-bits as coisas ficaram um pouco mais fáceis. Finalmente a gente deixou pra trás essa ideia de ficar manualmente controlando memória segmentada e passamos a lidar com endereços lineares, mais parecido com lidar com arrays em qualquer linguagem. Quer uma posição específica na memória? Vai direto na posição em vez de ficar fazendo conta de segmento e offset. O único problema é que no fim dos anos 80 onde o normal era lidar com no máximo 1 megabyte, pular pra 4 gigabytes parecia tão difícil quanto mandar um foguete pra lua.</p>
<p>Eu disse que no meio dos anos 80, se desse pra comprar 4 gigabytes iria custar mais de um milhão de dólares. Uns 15 anos depois, em 1999, já era possível comprar 4 gigabytes. Apesar de ainda ser caro não era mais um valor impossível. Com menos de 10 mil dólares dava pra comprar. Um usuário doméstico não ia comprar e nem precisava de tudo isso. Mas em corporações que precisavam, agora tinha a opção. Ou seja, em menos de 2 décadas a memória ficou 100 vezes menos cara. E o limite de 32-bits já tava sendo batido em empresas que precisavam rodar bancos de dados, por exemplo.</p>
<p>Pra remediar isso inventaram Address Windowing, que é a técnica de reservar uma janela de memória dentro dos 32-bits pra endereçar mais páginas fora. Parecido de novo com a idéia de mappers de nintendinho ou EMS no DOS. Particularmente o PAE ou Physical Address Extension, implementado nos processadores Pentium. Isso permitia um Windows 2000 Advanced Server acessar até 8 giga de memória real ou um Windows 2000 Datacenter acessar até 32 giga. No boot você passa o parâmetro de <code>/PAE</code> pra habilitar isso na CPU. Na prática, cada programa de 32-bits continua só conseguindo enxergar o máximo de 4 giga de memória virtual. Mas se dois programas quiserem realmente encher esse tanto de memória, num Windows 2000 normal ou ia cair pra swap ou ia dar erro.</p>
<p>Aliás, nem falei disso. Em modo real, o programa enxerga a memória real do sistema. Se for 1 megabyte, vai ser só 1 megabyte e o programa que se vire pra usar alguma técnica como memory mapped files, ou seja, tirar pedaços de dados da memória e gravar num arquivo, e quando precisar daquele pedaço, recarregar do arquivo pra memória. Pra fazer isso, num sistema operacional moderno, o gerenciador de memória do sistema operacional divide a memória em páginas. É como se fosse um array de arrays. Mais corretamente, acho que é uma árvore B-Tree, como expliquei dois episódios atrás. Cada node era uma página de memória.</p>
<p>Quando a memória vai acabando, o gerenciador pode pegar algumas páginas pouco usadas e gravar em disco pra liberar espaço. Disco é ordens de grandeza mais lento. Mas entre acabar a memória e crashear ou ficar mais lento, a escolha costuma ser deixar mais lento. Mas aí quando o programa tentar ler uma página que não tá mais lá, vai dar falha de leitura de página, o famoso <code>page fault</code>, uma exceção.</p>
<p>O gerenciador pausa o programa pra tratar essa exceção, se não tiver espaço sobrando na memória, primeiro grava mais páginas em disco pra liberar espaço, e depois carrega a página que você queria do disco de volta pra memória. Feito isso devolve o controle pro programa, e ele continua como se nada tivesse acontecido. É o mesmo conceito que a gente usa pra fazer cache. Se você já teve que otimizar tempo de resposta de aplicação Web com um memcache ou Redis, é o mesmo conceito no gerenciador de memória.</p>
<p>Você troca performance pra poder ter mais espaço do que caberia na memória real. Por isso falamos em memória virtual: o programa acredita que pode usar até 4 gigabytes, usando endereços de 32-bits. Ele pede essa posição de memória pro gerenciador de memória do sistema operacional. E esse gerenciador é que mapeia do endereço virtual pro endereço real. Por isso no 286 começaram a chamar esse esquema de modo protegido, porque quando dois programas enxergavam a mesma memória real, um podia escrever em cima dos endereços do outro. Mas agora eles enxergam uma tabela abstrata que o gerenciador de memória dá pra eles. Eles não tem acesso à memória real mais. Então esse gerenciador garante que um programa nunca vai acessar endereços do outro.</p>
<p>Com PAE é mais ou menos isso, ele tem mais tabelas virtuais e pede pra CPU mais memória, e ele vai dar memória acima do limite de 4 gigabytes. Do 386 em diante a própria CPU passou a ter um MMU que é Memory Management Unit, em hardware, que em CPUs com recurso de PAE vai ter endereço de 40 bits, e isso dá pra mapear memória real até o máximo de 1 terabyte. Só que ficar gerenciando essas tabelas virtuais também gasta espaço na memória. Você usa memória pra gerenciar mais memória, por isso a gente não tenta mapear o máximo teórico que na prática ninguém vai usar, pra não desperdiçar. Por isso o Windows de 32-bits limita de 8 giga até 64 gigas dependendo da versão.</p>
<p>Como expliquei antes, CPUs de 8-bits antigas tinham endereçamento de 16-bits. CPUs de 16-bits começaram tendo endereços de 20-bits, depois 24-bits. Aí saltamos pra processadores de 32-bits com endereços de 32-bits mesmo. Mas com PAE passamos a ter endereços de até 40-bits. Isso quem dá suporte é a CPU. Então um 386 de 32-bits não tinha como acessar mais que 4 gigas. Só com os Pentium de 32-bits e endereços de 40-bits que dava pra ir pra 32 gigas, 64 gigas ou até mais.</p>
<p>Outro problema é que você pensa que um programa tem acesso do endereço 0 até o endereço 4 bilhões e tanto que são os 4 gigabytes, de novo, como se fosse um lista linear. Pra visualizar na cabeça é melhor pensar assim mesmo. Mas internamente a memória virtual é segmentada como eu expliquei nos episódios de estrutura de dados. O sistema operacional precisa manter estruturas de dados como Heap, Stack e muito mais e pra isso ele reserva 2 gigabytes de endereço. Isso no caso do Windows. No Linux você podia compilar configurado pra reservar menos. E nos Windows Server você pode passar o parâmetro &ldquo;/3&rdquo; se não me engano, pro sistema operacional reservar só 1 giga pra ele e deixar 3 gigas pro programa.</p>
<p>Lembra que eu falei que tabelas virtuais de PAE usam espaço de memória? Então, esse é um exemplo de porque precisa reservar endereços. Você nunca tem o espaço total de endereços disponíveis. Um banco de dados com o SQL Server, rodando num Windows Datacenter, com PAE, com o flag /3 pra pedir mais memória, ainda assim vai ter no máximo 3 gigabytes. Então o que ele faz é subir mais de um processo, cada um com até 3 gigabytes. Se o servidor da época instalar o máximo de 64 gigabytes que o Windows Datacenter permitia, ele ia precisar subir e coordenar até 20 processos rodando em paralelo pra conseguir usar toda a memória real.</p>
<p>Uma coisa que eu ficava cabreiro naquela época é justamente só ter 2 gigas no máximo e eu ficava pensando: se existe essa opção de bootar com “/3” porque o Windows já não faz isso sempre por padrão pra me dar esse 1 giga a mais? E a resposta é porque nesse espaço reservado do sistema operacional fica mapeado os endereços pra bibliotecas compartilhadas, DLLs de sistema. E mesmo não ocupando os 2 giga ele pode ocupar endereços no fim desses 2 gigas.</p>
<p>Mesmo tendo espaço vazio no meio, se ele usar os endereços mais pertos do fim, eu não consigo diminuir o espaço que ele reserva. Daí tem o conceito de fazer rebasing das DLLs, que é forçar elas a carregar nos endereços mais do começo pra eu poder diminuir esse espaço com o boot em “/3”. Pra isso precisava carregar o mínimo de DLLs possível. Drivers podiam se carregar em endereços no algo, por isso que só opções de servidor, com drivers controlados, é que costumavam dar essa opção.</p>
<p>Então, gerenciar memória nunca foi algo legal, sempre foi chato. Felizmente, diferente do que a gente precisava fazer com DOS, era menos chato se você programasse no mundo 32-bits porque nesse estágio Windows ou Linux já faziam o grosso do gerenciamento de memória e você só tinha que lidar com a memória virtual disponível por processo que eles dão pra você, mas quando viramos pro século XXI, era da Internet e servidores de bancos de dados, claramente a gente ia precisar de mais memória. 4 gigas pareciam infinitos em 1995, mas em 2005 era altamente insuficiente.</p>
<p>Por isso começou a corrida pros 64-bits no começo do século, com AMD Athlon, Intel Itanium e outros. Mas em resumo, hoje a maioria das pessoas estão em 64-bits, inclusive em celulares. 64-bits dá um total teórico de 2 elevado a 64 endereços que são inimagináveis 16 mil PETA bytes. Assim como em 1985 ninguém conseguia imaginar uma máquina com 4 gigabytes de RAM, em 2021 ninguém consegue imaginar uma máquina com 16 mil Peta bytes. E felizmente hoje, 4 giga DDR4 custa menos de 100 dólares. Nada mal partindo de 10 mil dólares no fim dos anos 90.</p>
<p>Porém, se você for milionário e quiser ir até o limite teórico de 64-bits que são os 16 mil petabytes, precisaria gastar na faixa de 400 bilhões de dólares. Pra dar uma noção, em 2019 se estimava que o Google inteiro tinha pouco mais de mil petabytes de dados, então estamos falando do custo pra guardar todos os dados do Google na RAM de uma única máquina, dez vezes.</p>
<p>Mesmo se você fosse um Jeff Bezos ou Jack Ma da vida pra jogar uns bilhões fora, ainda assim não ia conseguir usar esse tanto de memória. Isso porque ninguém imagina que a gente vai chegar perto desse número tão cedo, talvez nas próximas décadas. Então é desperdício otimizar o chip pra ter esse tanto de pinos de endereço e barramentos e controladores. Então o chip de fato tem registradores de 64-bits pra poder calcular números grandes numa única instrução. Mas o barramento de endereços é de 40 ou no máximo 48-bits, e não 64.</p>
<p>40 bits é um intervalo de endereços que vai de zero até 2 elevado a 40 que é exato 1 petabyte. Isso permite endereçar 16 mil vezes menos memória do que 64-bits. Nenhuma CPU moderna de 64-bits, seja um Intel Xeon, um AMD Epic, um Apple M1, um Qualcomm Snapdragon. Todos são capados na faixa dos 40-bits. Dificilmente alguém vai ter 1 petabyte de RAM numa única máquina. O mais comum em workstations hoje é a faixa de 256 giga. Um Mac Pro pode ir até 1.5 terabytes de RAM, que são 12 pentes de 128 giga DDR4. O custo hoje em 2021 pra isso é 15 mil dólares.</p>
<p>Com o antigo PAE no mundo 32-bits começamos a pensar um pouco mais na segurança dos dados na memória. Com tudo que expliquei deve ter ficado claro que naquela época a gente precisava ficar caçando kilobytes em tudo que é canto pra carregar drivers. Então adicionar coisas extras como aparatos de segurança não só não era prioridade, como simplesmente não ia caber em muitos casos. Um vírus é minúsculo. Mas um antivírus é bem pesado.</p>
<p>Com a adoção em massa de computadores, especialmente por causa da adoção da Internet, surgiram muitos vírus e malwares e eles sempre tiram vantagem do entendimento de como endereçamento de memória funciona e como conseguir injetar código malicioso em algum endereço que vai ser executado. Pra mitigar isso, junto com o PAE veio a primeira idéia do bit NX, que significa no-execute.</p>
<p>A ideia é marcar certas áreas de endereços de memória virtual que não podem ser executados. Eu não sabia disso, mas parece que nos 80286, com a memória segmentada daquele jeito chato que expliquei, também dava pra controlar isso no nível dos segmentos. Mas em memória com endereçamento linear de 32-bits a partir dos 386, já não tinha mais segmentos nos endereços virtuais. Então eles inventaram esse outro jeito de conseguir saber quais endereços tem dados que não podem ser executados. Até então não existia essa distinção.</p>
<p>Isso é importante porque um programa pode ter instruções pra dar jump pra qualquer lugar. Se ele tiver um bug e tentar pular pra um pedaço de memória que só tem dados, vai dar crash e só. Mas se alguém notar esse comportamento e escrever instruções executáveis neste endereço, quando o bug acontecer e der jump, vai executar o código malicioso sem saber. Mas se o sistema operacional marcar como no-execute, esse código não pode ser executado e se tentar, a CPU vai impedir.</p>
<p>Esse bit NX permite o recurso de DEP de segurança que é Data Execution Prevention, que vai evitar que certas partes da memória sejam executáveis, como a stack dos programas. Em máquinas mais antigas que não tinham o bit NX ainda é possível ter recurso parecido de no-execute. Se os endereços fossem de 32-bits flat, lineares, seria mais difícil, mas isso são os endereços virtuais, como pra calcular o endereço real os chips da Intel precisam fazer aquele cálculo de segmento e offset, dá pra marcar regiões da memória por segmento. Se você entendeu tudo que expliquei hoje já consegue imaginar e vou deixar um link pra uma resposta que achei no site StackExchange que explica em mais detalhes se tiver curiosidade.</p>
<p>Mas pra fechar o assunto de memória, como cheguei até 64-bits, não daria pra finalizar sem pelo menos mencionar ASLR ou Address Space Layout Randomization. A gente escovou bastante bits hoje de propósito pra você entender que tudo funciona baseado em endereçamento. Você entendeu que a memória é segmentada. Segmentos são simplesmente conjuntos de endereços. Do endereço 0 até X fica a stack, do X+1 até Y fica a heap, de Y+1 até Z ficam endereços de periféricos, e assim por diante. Era assim no nintendinho de 8-bits, no 80286 de 16-bits, e assim por diante.</p>
<p>Lembra toda a conversa sobre o limite de 1 megabyte, memória expandida e tudo mais? Pois bem, pelos motivos de se ter poucos endereços e também pra ser mais fácil de programar em baixo nível, os segmentos tinham endereços fixos. Se você for programar num nintendinho, tem certeza que vai achar a ROM a partir do endereço hexa 4020, por exemplo. Se for programar pra DOS num PC antigo, tem certeza que acima do endereço de 640 kilobytes vai achar os endereços de periféricos. E a mesma coisa se perpetuou em modo protegido, memória virtual e sistemas operacionais de 32-bits.</p>
<p>Eu expliquei que o Windows reserva um pedaço da memória virtual de 4GB pra segmentos internos. O tal  espaço reservado de 2 gigabytes. Lá vai ter coisas como a stack e o heap do programa, mas também DLLs que são bibliotecas compartilhadas do sistema. Quando seu programa precisar de alguma função do sistema, ela vai estar num endereço que você consegue enxergar dentro do seu espaço protegido. O problema é que tudo isso sempre vai estar em endereços fixos e conhecidos.</p>
<p>Se um vírus ou malware se injetar no seu programa, ele vai ter acesso a esse espaço de memória. Pior, se ele souber de um bug como um stack overflow, vai saber exatamente em que endereço fica a stack e vai saber exatamente em qual endereço injetar seja lá que código malicioso pra executar. Em 32-bits isso é muito fácil. Se você é gamer, muitos mods funcionam assim: se injetando nos programas. O jeito mais educado é interceptando chamadas de funções. O jeito bruto é escrevendo direto em endereços fixos na memória do jogo.</p>
<p>Uma forma de dificultar isso é carregar esses segmentos e DLLs em endereços aleatórios. Só na hora que o programa executar que ele vai saber quais são os novos endereços, daí um vírus não poderia assumir um endereço fixo, porque toda vez vai mudar. O problema é que em 32-bits temos pouco espaço e se ficar movendo segmentos de lugar a gente vai fragmentar a memória. Faz de conta que resolvemos colocar uma DLL no meio dos 2 GB que temos. Daí já impede aquele parâmetro “/3” pra gente conseguir ter 3 gigas livres pro programa. Aí pra servidor esse programa já não presta.</p>
<p>Mas acabamos de falar que em 64-bits tem tanto endereço, mas tanto endereço, que na prática os chips de CPUs como da Intel e AMD só vem mesmo com 40-bits de endereços reais. Significa que temos 24-bits de endereços sobrando que não dá pra usar de qualquer jeito. Então ASLR é um recurso que calcula uma posição aleatória dentro de X posições na memória virtual, estrategicamente escolhidas pra não fragmentar a memória de formas absurdas. Toda vez que seu programa iniciar o endereço da stack, da heap, das DLLs compartilhadas vão ser diferentes dentro de 256 possibilidades.</p>
<p>Tem como o vírus descobrir o endereço em tempo real se tiver vazamento de ponteiros no seu programa, mas esse recurso sozinho dificulta uma quantidade grande se não todos os malwares de overflow que existiam até 2004 pelo menos. Isso sozinho não torna seu computador totalmente seguro, só cria uma camada extra de dificuldade. Se estiver interessado em saber formas de burlar essa proteção também vou deixar linkado um artigo de 2020 que mostra como nas descrições abaixo.</p>
<p>Como eu disse no começo, você nunca vai ter que calcular endereços de 20-bits em computadores de 16-bits dos anos 80, felizmente. Em meio a HIMEM.sys e motoristas bêbados, até chegarmos na era dos 64-bits, gerenciar memória era uma tarefa árdua, tediosa, cheia de bugs e exploits. Tinha mais técnicas de gerenciar memória e escovar bit do que existem pokemóns. Mas acho importante entender como a memória funciona, porque mesmo programadores experientes tratam esse assunto mais como astrologia do que como ciência. Daí quando chegamos no limite da máquina e precisamos pensar em formas de otimizar, a discussão parece mais argumentação de horoscopo do que realmente formas práticas.</p>
<p>Parece besteira, mas entender um mapper de nintendinho de 8-bits, faz a gente entender memória expandida em DOS de 16-bits, que faz a gente entender PAE em computadores de 32-bits, até ter uma visão mais realista do que são 64-bits. A forma como gerenciadores de memória lidam com segurança, fragmentação, páginas e swaps, vai ajudar quando você precisar otimizar um sistema.</p>
<p>Se tiver os conceitos que eu expliquei hoje vai conseguir ler um paper do Google sobre a diferença entre um tcmalloc, jemalloc ou um Scudo hardener. Memória não é tudo igual. Não é uma lista linear. Quanto mais cedo você entender como seus programas realmente usam a memória, mais cedo vai parar de sugerir bobagens pra otimizar seus programas. Se ficaram com dúvidas mandem nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal, clique no sininho e compartilhem o video com seus amigos. A gente se vê, até mais.</p>
]]></content:encoded><category>ciencia-da-computacao</category><category>historia-da-computacao</category><category>seguranca</category><category>akitando</category></item><item><title>[Akitando] #95 - Árvores: O Começo de TUDO | Estruturas de Dados e Algoritmos</title><link>https://akitaonrails.github.io/2021/04/06/akitando-95-arvores-o-comeco-de-tudo/</link><guid isPermaLink="true">https://akitaonrails.github.io/2021/04/06/akitando-95-arvores-o-comeco-de-tudo/</guid><pubDate>Tue, 06 Apr 2021 10:30:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/9GdesxWtOgs"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Este é o final da minha Trilogia de Estruturas de Dados e Algoritmos e finalmente vou conseguir falar sobre o mais importante na matéria: árvores! Vamos de BSTs a AVLs, passando por Red Black Trees e muito mais! Se você nunca estudou sobre isso, ou não tinha entendido quando estudou, esta é sua chance!&lt;/p&gt;
&lt;p&gt;Conteúdo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;02:27 - Distribuição Gaussiana&lt;/li&gt;
&lt;li&gt;05:53 - Hashing&lt;/li&gt;
&lt;li&gt;09:43 - Todo mundo erra&lt;/li&gt;
&lt;li&gt;10:18 - Recapitulando&lt;/li&gt;
&lt;li&gt;12:34 - Pilhas e Filas&lt;/li&gt;
&lt;li&gt;12:57 - Grafos&lt;/li&gt;
&lt;li&gt;13:25 - Entendendo Redes&lt;/li&gt;
&lt;li&gt;15:05 - Entendendo Árvores&lt;/li&gt;
&lt;li&gt;16:34 - Heap/Memória&lt;/li&gt;
&lt;li&gt;18:21 - Construindo Árvores&lt;/li&gt;
&lt;li&gt;19:31 - Vantagem da Partição (log)&lt;/li&gt;
&lt;li&gt;20:18 - Árvore de Procura Binária&lt;/li&gt;
&lt;li&gt;22:24 - Visualizando Inserção na Árvore&lt;/li&gt;
&lt;li&gt;26:19 - Imprimindo a Árvore Ordenada&lt;/li&gt;
&lt;li&gt;30:24 - Procura Binária&lt;/li&gt;
&lt;li&gt;32:01 - Complexidade de Recursos&lt;/li&gt;
&lt;li&gt;34:52 - Pior Caso&lt;/li&gt;
&lt;li&gt;35:47 - Red Black Trees&lt;/li&gt;
&lt;li&gt;38:33 - Cor e Rotação&lt;/li&gt;
&lt;li&gt;42:52 - AVL Trees&lt;/li&gt;
&lt;li&gt;44:51 - Red Black no Mundo Real&lt;/li&gt;
&lt;li&gt;47:06 - B-Tree e B+ Tree&lt;/li&gt;
&lt;li&gt;48:47 - Bancos de Dados&lt;/li&gt;
&lt;li&gt;52:46 - Intuição em Algoritmos&lt;/li&gt;
&lt;li&gt;54:13 - Por que estudar tudo isso?&lt;/li&gt;
&lt;li&gt;57:04 - Conclusão&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meus Vídeos Mencionados:&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/9GdesxWtOgs"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Este é o final da minha Trilogia de Estruturas de Dados e Algoritmos e finalmente vou conseguir falar sobre o mais importante na matéria: árvores! Vamos de BSTs a AVLs, passando por Red Black Trees e muito mais! Se você nunca estudou sobre isso, ou não tinha entendido quando estudou, esta é sua chance!</p>
<p>Conteúdo:</p>
<ul>
<li>00:00 - Intro</li>
<li>02:27 - Distribuição Gaussiana</li>
<li>05:53 - Hashing</li>
<li>09:43 - Todo mundo erra</li>
<li>10:18 - Recapitulando</li>
<li>12:34 - Pilhas e Filas</li>
<li>12:57 - Grafos</li>
<li>13:25 - Entendendo Redes</li>
<li>15:05 - Entendendo Árvores</li>
<li>16:34 - Heap/Memória</li>
<li>18:21 - Construindo Árvores</li>
<li>19:31 - Vantagem da Partição (log)</li>
<li>20:18 - Árvore de Procura Binária</li>
<li>22:24 - Visualizando Inserção na Árvore</li>
<li>26:19 - Imprimindo a Árvore Ordenada</li>
<li>30:24 - Procura Binária</li>
<li>32:01 - Complexidade de Recursos</li>
<li>34:52 - Pior Caso</li>
<li>35:47 - Red Black Trees</li>
<li>38:33 - Cor e Rotação</li>
<li>42:52 - AVL Trees</li>
<li>44:51 - Red Black no Mundo Real</li>
<li>47:06 - B-Tree e B+ Tree</li>
<li>48:47 - Bancos de Dados</li>
<li>52:46 - Intuição em Algoritmos</li>
<li>54:13 - Por que estudar tudo isso?</li>
<li>57:04 - Conclusão</li>
</ul>
<p>Meus Vídeos Mencionados:</p>
<ul>
<li>Monetizar? Bloquear ADs? O que Fazer? (<a href="https://www.youtube.com/watch?v=dOe9X6Q_-nU"target="_blank" rel="noopener">https://www.youtube.com/watch?v=dOe9X6Q_-nU</a>)</li>
<li>Entendendo Conceitos Básicos de CRIPTOGRAFIA | Parte 1/2 (<a href="https://www.youtube.com/watch?v=CcU5Kc_FN_4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=CcU5Kc_FN_4</a>)</li>
<li>O Guia +Hardcore de Introdução à COMPUTAÇÃO (<a href="https://www.youtube.com/watch?v=8G80nuEyDN4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=8G80nuEyDN4</a>)</li>
<li>O Computador de Turing e Von Neumann | Por que calculadoras não são computadores? (<a href="https://www.youtube.com/watch?v=G4MvFT8TGII"target="_blank" rel="noopener">https://www.youtube.com/watch?v=G4MvFT8TGII</a>)</li>
<li>Turing Complete, Emuladores e o Chip ARM M1 (<a href="https://www.youtube.com/watch?v=kz3649U2sJY"target="_blank" rel="noopener">https://www.youtube.com/watch?v=kz3649U2sJY</a>)</li>
<li>Qual a REAL diferença entre Arquivos Binário e Texto?? 🤔 (<a href="https://www.youtube.com/watch?v=oSCVb4Ts-G4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=oSCVb4Ts-G4</a>)</li>
<li>Hello World Como Você Nunca Viu! | Entendendo C (<a href="https://www.youtube.com/watch?v=Gp2m8ZuXoPg"target="_blank" rel="noopener">https://www.youtube.com/watch?v=Gp2m8ZuXoPg</a>)</li>
<li>O que vem DEPOIS do Hello World | Consertando meu C (<a href="https://www.youtube.com/watch?v=YyWMN_0g3BQ"target="_blank" rel="noopener">https://www.youtube.com/watch?v=YyWMN_0g3BQ</a>)</li>
</ul>
<p>Links:</p>
<ul>
<li>Red-black Trees (rbtree) in Linux (<a href="https://www.kernel.org/doc/Documentation/rbtree.txt"target="_blank" rel="noopener">https://www.kernel.org/doc/Documentation/rbtree.txt</a>)</li>
<li>Red-black Trees Animated (<a href="https://www.cs.usfca.edu/~galles/visualization/RedBlack.html"target="_blank" rel="noopener">https://www.cs.usfca.edu/~galles/visualization/RedBlack.html</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>No final eu acabei me empolgando no assunto de algoritmos e estruturas de dados. Eu não queria continuar porque dá bastante trabalho fazer esse tipo de episódio, mas já que cheguei até aqui, me senti na obrigação falar da estrutura de dados que eu considero a mais importante da matéria: árvores ou trees.</p>
<p>Os vídeos acabaram trazendo muita gente nova que acho que não entendeu a proposta do canal. Eu não tô fazendo um substituto pra cursos oficiais de faculdades, e eu nem tenho intenções de parecer que seja. Só tô discutindo assuntos que acho que são importantes que iniciantes procurem estudar por conta própria. E pra isso eles precisam pelo menos saber que esses assuntos existem e porque são importantes.</p>
<p>Pra quem não sabe, o canal não tem fins lucrativos, eu não tenho patreons, nem patrocínios de nenhum tipo, nem vendo nada. E tudo que é arrecadado com ads eu faço doação de 200%, quer dizer, eu pego o que arrecado e dobro do meu bolso pra outros projetos de educação sem fins lucrativos. Vejam meu video acima explicando e não esqueçam de mandar mais sugestões de projetos pro e-mail contato @ codeminer42.com.</p>
<p>Como eu faço os vídeos nas minhas horas vagas e como desde o fim do ano passado meu volume de trabalho aumentou drasticamente, tem sobrado bem menos tempo pra me dedicar. Infelizmente não é porque tô jogando muito videogame. Meu pobre PS5 tá pegando pó desde que comprei. E eu sou meio retardado das idéias e decidi justo fazer videos de 1 hora de duração, trabalhosos de editar, e obviamente cometi erros pra lá e pra cá. Desculpem por isso, mas lembrem que eu relato os principais nas erratas na descrição e até mesmo no video seguinte. Por outro lado acho bacana que vocês estão prestando atenção e notando esses erros.</p>
<p>E só pra variar, o comecinho do episódio de hoje vai ser pra eu falar das erratas do episódio anterior. Apesar de pra alguns parecer coisa pequena, é importante eu explicar o correto, porque se você entender completamente esta minissérie de 3 episódios, talvez comece a enxergar programação e código de uma forma diferente. E mais do que isso: que cometer erros faz parte do processo. Então vamos lá!</p>
<p>(&hellip;)</p>
<p>Um erro besta, mas importante, é quando eu falo sobre a função de hashing pro nosso exemplo de Hashtable. Sou o primeiro a dizer que as pessoas não devem achar que tudo é uma distribuição gaussiana ou normal no mundo, porque não são. E eu mesmo cometi o erro de fazer isso. Vamos entender rapidamente porque isso é importante e aproveitar pra fazer umas tangentes.</p>
<p>Eu não vou explicar estatística porque eu não me acho a pessoa mais adequada. Mas todo mundo já deve ter visto a distribuição gaussiana ou normal. Todo tipo de evento independente pode ser gaussiano, por exemplo a altura média da população. Faz de conta que se medirmos a altura de 100 pessoas, 80 vão estar nos 1 metro e sessenta, 12 vão estar perto de 1 metro e meio ou 1 metro e oitenta, e 8 entre 1 metro e quarenta e 1 metro e noventa. Se colocar isso num gráfico, mesmo medindo poucas pessoas, a gente começa a ver o tal formato de sino ou bell shape, que caracteriza uma distribuição normal.</p>
<p>A média é onde costuma estar a maioria das medições, no meio. E quanto mais pra direita ou pra esquerda vai indo, as medições caem rápido. Essas distâncias a gente chama de sigmas. Se não me engano, 65 porcento das medições vão estar em sigma 1, 95 porcento em sigma 2, e 99 ponto sete porcento em sigma três. Quem estudou coisas como processos de controle de qualidade conhece Six Sigma, ou sigma 6 que é o famoso 99 ponto 99966 ou 3.4 defeitos por milhão.</p>
<p>Quando a gente fala 99.7% parece muito bom, porque nas nossas concepções mais casuais, parece &ldquo;perto de 100 porcento o suficiente&rdquo;, mas não é. Um contrato de SLA Sigma 3 de 99.7% significa 1 dia inteiro fora do ar num ano, por exemplo. Em uma indústria que produz milhões de itens por mês, significa 3 MIL defeitos por milhão de itens, por mês. Infelizmente, perfeição, ou seja, zero defeitos é quase impossível no mundo real por períodos prolongados de tempo. Eventualmente vai ter erros humanos, falhas mecânicas. Six Sigma costuma ser a meta.</p>
<p>A última coisa a se lembrar sobre distribuições normais e médias é que elas só valem pra eventos independentes entre si. Quando você mensura coisas onde um não afeta o outro, como jogar dados, peças fabricadas em massa, altura de uma população. Mas um lugar onde todo mundo usa errado é salários, porque o salário de um influencia o dos demais, não é um evento independente. É um número dinâmico que varia por diversos fatores. Outras distribuições estatísticas vão se encaixar melhor como a Lei de Potência ou Power Law, mas não a gaussiana.</p>
<p>Enfim, essa tangente não tem nada a ver com a minha errata, foi só algumas coisas que eu já  queria compartilhar mesmo. Mas a errata é que na explicação sobre funções de hashing eu disse que o ideal seria uma função que conseguisse devolver números que caracterizam uma distribuição gaussiana. Mas tá errado, pois significa que na média ele ia devolver o mesmo valor, com alguns números diferentes próximos da média. Por exemplo, se fosse uma função que devolve números entre 1 e 10, ele poderia só ficar devolvendo o número 5 por exemplo, que é exatamente o oposto do que a gente quer.</p>
<p>Na verdade a distribuição pra esse tipo de função deve ser uniforme onde, aí sim na média, ele devolve a mesma quantidade pra cada número do range. E por isso minha confusão antes. No exemplo, se for uma função que devolve números de 1 a 10, e eu rodar essa função cinquenta vezes, cada número de 1 a 10 deveria aparecer na média 5 vezes, e assim balancear o uso do meu hashtable.</p>
<p>E mesmo assim não deveria devolver cinco números 1s, depois cinco números 2s etc de forma fácil de prever, determinístico, e aí eu disse que deveria ser o mais próximo de aleatório quanto possível, se mantendo numa distribuição uniforme. E eu sei que isso que eu acabei de falar, por si só é assunto pra outro episódio, então pesquisem sobre números pseudo-aleatórios pra mais detalhes depois.</p>
<p>Continuando, eu usei o exemplo de função de hashing djb2 e disse que não é necessariamente a melhor, mas não tá correto também, na realidade a djb2 é uma boa função se você não souber nenhuma outra. Como disse antes, o importante é os resultados caírem o mais perto possível de uma distribuição uniforme e evitar colisões, ou seja, evitar devolver muito do mesmo resultado.</p>
<p>Dentre as funções de hashing mais conhecidas, um exemplo didático é o Lose Lose, um dos piores, muita colisão. Tem o Super Fast Hash, que como o nome diz, é rápido, mas também muita colisão. E tem o CRC32 que é bom mas precisa de um lookup table de 1 kilobyte, então consome mais recursos que outros algoritmos, o que pode fazer diferença num ambiente limitado, como embarcado, mas é bom pra todo o resto. Não tenho certeza de cabeça, mas acho que é usado em coisas como protocolos de rede, por exemplo.</p>
<p>O que eu venho explicando desde os episódios sobre Turing como tudo são números. Endereços de memória são números. Strings é uma cadeia de bytes, tudo é um numerozão binário. E hashing é uma forma de conseguir números menores que representam um número maior, como Strings. Então um texto da wikipedia é um linguição de bits gigantes, um numerozão enorme. E com hashing podemos etiquetar esse textão com um número menor. No caso de um SHA-256 da vida, seria com uma etiqueta numérica de 256 bits. É como um número de série de um item. O ideal é ser um número único, mas não é garantia e por isso temos colisões onde dois itens diferentes, dois textões diferentes de wikipedia podem chegar no mesmo hash.</p>
<p>Quem já programa poderia pensar, mas porque não usamos coisas como SHA-512? Eles são usados pra segurança, não são melhores? Eles tem mais ou menos a mesma função, devolver um número que representa uma cadeia de bits, um String, como a chave do nosso hash. E esse número costuma ser o mais único quanto possível. Nunca vai ser 100% sempre único, mas quanto maior o valor do resultado, estatisticamente menores as chances disso acontecer.</p>
<p>Porém a categoria de funções de hashing como SHA-512 foram desenhados pra ter características de segurança, se usados em conjunto com coisas como salts, como eu expliquei no episódio de criptografia. São mais avançados e consomem mais recursos e mais processamento pra gerar esse número. Portanto, não são necessariamente eficientes como geradores de chaves num hashtable se não há requerimentos de segurança junto.</p>
<p>Em particular o SHA-512 é rápido em máquinas de 64-bits porque as funções de cálculo vem implementado em hardware no CPU hoje em dia. De qualquer forma não quer dizer que não poderia ser usado, só que não vai ser o mais eficiente pra hashtables genéricos. E voltando pra errata, eu falei no video que o cálculo do djb2 faz bitwise shift de 5 bits pra esquerda, que é um cálculo rápido e é equivalente a multiplicar por 3 em decimal. O certo é multiplicar por 32.</p>
<p>Eu ainda cometi dois erros sobre complexidade. O primeiro foi quando falei que NP é tempo não polinomial, mas o correto, que até falo depois no vídeo, é que NP é Non Deterministic Polynomial Time ou Tempo polinomial não determinístico. Outro erro foi quando falei que complexidade exponencial é Big O de 2 elevado a N mas na realidade é N elevado a N. E falando nisso eu disse que fatorial é a complexidade mais cara, mas na realidade é a exponencial, porque por exemplo, 15 fatorial é 15 vezes 14 vezes 13 vezes 12 etc até 1. Mas 15 elevado a 15, exponencial, é 15 vezes 15 vezes 15 etc 15 vezes.</p>
<p>Exemplo de trecho que tirei direto da cabeça e não revisei direito. Eu disse que um exemplo de complexidade fatorial é a implementação do cálculo de fatorial recursivo. Tá errado e nem lembro porque eu falei isso, mas na realidade é O linear e não fatorial. O exemplo correto pra fatorial é o Travelling Salesman, que eu traduzi direto do inglês pra Vendedor Viajante mas no Brasil povo tá acostumado a falar em problema do caixeiro viajante. Na real tanto faz, se você procurar em português vai achar como caixeiro mesmo, mas eu recomendo procurar como travelling salesman que vai achar artigos muito melhores sobre o problema.</p>
<p>Tudo isso dito, vamos ver se neste último episódio da série eu cometo menos erros. Sempre leiam os comentários, tem um povo que tá prestando atenção e corretamente corrigindo. Falando nisso eu fico surpreso que tem gente que acha que pessoas como eu não erram. Óbvio que erram e não é pouca coisa, no máximo eu diria que pessoas experientes no geral reconhecem o erro mais rápido e corrigem mais rápido. Pessoal inexperiente, por alguma razão, insiste no erro por muito mais tempo do que deveriam. No final todo mundo erra, então não fiquem frustrados se vocês erram, o problema é errar a mesma coisa o tempo todo.</p>
<p>Pra você não se perder vamos recapitular. Ano passado eu adaptei dois vídeos de dois canais que eu gosto, do Ben Eater e do Gaming Mechanics Explained. Primeiro pra mostrar no nível do transistor, num protoboard, como bits são armazenados numa memória e como instruções de &ldquo;baixo nível&rdquo; são construídas com transistores, a verdadeira linguagem de máquina. Tudo começa com instruções simples como soma, construído em hardware, passando elétrons por transistores, formando portas lógicas. E com isso você chega no Assembly.</p>
<p>Depois, de propósito, resolvi contar a história de Turing e Von Neumann pra explicar como instruções e dados populam a mesma linguiçona de bits e o que isso significa, os primórdios da computação moderna. Na sequência eu quis falar sobre tipos de arquivos, que todo mundo usa e ninguém entende que arquivos texto e arquivos executáveis é tudo a mesma coisa, tudo um linguição de bits. Todo programa, todo arquivo, é nada mais que um numerozão gigante.</p>
<p>A ordem que esses bits aparecem nessa fita determina seu formato. Cada bit tem um endereço, que é a posição na fita. E quando você tem sequências especiais de bits em determinados endereços vai começar a diferenciar entre uma imagem jpeg e um executável de Linux, por exemplo. Aliás, quando eu falei de memória virtual nos dois últimos episódios, a divisão de segmentos como stack, heap, ROData, Text é específica de Linux e executáveis ELF. Windows e Mac o layout dos segmentos é diferente, ou seja, o range de endereços de cada coisa vai ser diferente. Mesma coisa entre arquiteturas de 32 ou 64-bits.</p>
<p>Enfim, daí chegamos na trilogia de agora, onde comecei com diferentes Hello World pra mostrar como essa fita é usada e como a memória começa a ser organizada em coisas como stack e heap e finalmente no último episódio mostrei como existem diferentes tipos de listas: arrays, listas ligadas, hashtables. Não cheguei a mencionar, mas você pode implementar Stacks que são pilhas e Queues que são filas usando qualquer uma dessas estruturas. Números inteiros, arrays de chars, e coisas assim são tipos primitivos, coisas “concretas”. Pilhas ou filas são conceitos, que podem ser implementados de vários jeitos, e por isso chamamos de tipos abstratos.</p>
<p>Pilhas podem ser arrays ou listas ligadas onde você só tem acesso ao último elemento, no caso de pilha, ou seja, só operações de push pra empurrar um elemento pro fim da lista e pop pra tirar o elemento do começo da lista. Filas você também tem duas operações principais, uma enqueue pra colocar no fim da lista e uma dequeue pra tirar do começo da lista. São casos de estruturas que chamamos de lineares.</p>
<p>Então também existem estruturas não-lineares, e aqui entra o assunto de Grafos. Eu não lembro se era correto dizer assim, mas um caso particular de Grafo unidirecional é uma Árvore. Voltando. Grafos você vê todos os dias. Toda rede, seja rede local ou de internet, é um grafo. Um conjunto de nós ou Nodes, como computadores, ligados por Edges que são tipo o cabo ethernet pro roteador ou mesmo a conexão wifi que liga seu smartphone no roteador.</p>
<p>Uma rede social é um grafo, onde os Nodes são as pessoas. Os Edges são os relacionamentos entre elas. E você pode ter todo mundo ligado com todo mundo ou diversos grupos ligados por algumas poucas pessoas em comum entre os grupos. Se quiser entender redes sociais, você começa entendendo Grafos.</p>
<p>Mas grafos é um assunto muito grande pra explicar em pouco tempo. Por hoje, entendam que existe essa matéria pra estudar. Praticamente tudo que você chama de Rede pode ser descrito com grafos. Por exemplo, o algoritmo de como encontrar o caminho mais curto entre dois Nodes num Grafo, que é o Shortest Path do Dijkstra. Poderia ser o caminho mais curto entre duas localizações num Google Maps ou o caminho mais curto no relacionamento de pessoas num LinkedIn.</p>
<p>E na estatística existem vários estudos sobre as particularidades de distribuição de diferentes grafos ou redes. Se quiser se aprofundar, recomendo livros de rede do Steven Strogatz e Duncan Watts, que inclusive tem um modelo que leva o nome deles, o modelo Watts-Strogatz. Entendendo redes de forma estatística você chega nos temas de small worlds ou mundos pequenos, os famosos seis graus de separação.</p>
<p>Acho que na faculdade o povo ensina até o modelo de Erdös e Rényi. O tema de mundos pequenos explodiu por conta do aparecimento das redes sociais massivas no começo do século XXI. Porque agora temos dados reais massivos pra analisar, o tal Big Data, que foi material de pesquisa pra um Duncan Watts quando trabalhou no Yahoo e hoje na Microsoft. Daí você chega nos modelos de Watts-Strogatz e Barabási-Albert. Se você acha que entende redes sociais e nunca ouviu falar deles, ainda não sabe o que são redes sociais.</p>
<p>Isso tudo dito, deixa eu dar uns passos pra trás. Depois de aprender arrays, listas ligadas e hashtables acho que é importante fechar com árvores que, por si só, é um assunto bem grande, podemos ir pra B-trees que sendo honesto eu não lembro mais tudo de cabeça. B-trees é o que tá por trás de índices de bancos de dados, por exemplo. Mas falando genericamente de árvores, você vai ver essa estrutura em tudo que é lugar.</p>
<p>Exemplos simples? Basta ver seu Windows Explorer, Mac Finder ou GNOME Nautilus. Seu file system, o sistema de arquivos é inteiro baseado em árvores. Diretórios e arquivos são Nodes na árvore, a raíz pode ser o seu C:. Veja um documento HTML, qualquer página da Web. Sua representação é o famoso DOM ou Document Object Model, uma árvore onde cada Node, ou entidade, são elementos como parágrafos, forms, imagens e tudo mais. E falando em objetos, veja um Java da vida cuja biblioteca é toda herdada a partir da classe Object. Toda herança de classes e interfaces é representada numa árvore.</p>
<p>E sem árvores você não consegue seguir pras próximas matérias na ciência da computação como sistemas operacionais ou compiladores. Todo seu código fonte, seja numa linguagem compilada como C ou interpretada como Python ou Javascript passa por etapas como análise léxica, depois análise sintática, que gera coisas como um AST ou abstract syntax tree, que é a representação do seu código texto no formato de uma árvore. E mesmo pra entender sistemas operacionais você vai precisar de árvores.</p>
<p>Quando falei de memória virtual vimos dois exemplos de segmentos importantes. Primeiro a stack, que controla o estado da execução do programa a cada instrução e que é implementado como uma estrutura de dados linear de Pilha. Já o segmento Heap pode ser gerenciado com uma árvore. Na realidade o Heap usa uma estrutura de dados baseada em árvore chamada Heap. Um Heap pode ser uma implementação de um tipo de dados abstrato chamado Fila de Prioridade ou Priority Queue. São mais tipos abstratos como a Pilha ou Fila, que podem ser implementadas com as estruturas de dados primitivas que expliquei antes.</p>
<p>Se for uma binary heap, então é implementado usando uma binary tree, ou árvore binária. A distinção é importante porque nos exemplos de file system ou DOM de HTML as árvores não são binárias, porque cada Node não tem só dois elementos, eles podem ter múltiplos filhos ou até múltiplos pais, então são grafos mas não árvores binárias. Provavelmente B-trees, que vou explicar o que são mais pro final.</p>
<p>A estrutura de Heap é a usada no algoritmo de ordenação Heapsort. Lembram quando falei de Quicksort no episódio anterior? Tem outras nos livros de algoritmos como mergesort, insertion sort; e heapsort costuma aparecer junto. Mas o importante é saber que sua memória tá organizada numa árvore binária.</p>
<p>Uma árvore tem como característica começar de um Node raíz ou Root. Root pra quem não sabe, é tradução pra raíz, o primeiro Node. O primeiro usuário num Linux chama Root, por isso. Quando você digita caminhos de diretórios a primeira barra ao contrário, backslash, a gente chama de root do file system. Num Windows o “C:\” só é a raíz do seu primeiro HD que o Windows sempre chama de C (e pra quem não sabia é porque A e B eram reservados pra drives de disquetes). Mas enfim, você vai ver esse nome root ou raíz em vários lugares.</p>
<p>Lembram da Lista Ligada? É uma struct que tem um valor qualquer que queremos guardar e um ponteiro pra outro Node, o previous ou next no nosso exemplo. Numa árvore, cada Node vai apontar pra mais de um Node. Numa árvore binária, em particular, aponta pra 2 outros Nodes que comumente chamamos de <code>left</code> e <code>right</code> ou esquerda e direita.</p>
<p>Uma árvore pode ser balanceada ou não-balanceada. Numa árvore binária, não balanceada, onde vamos incluindo elementos, digamos, tudo pra direita da árvore, vai se comportar basicamente como uma lista ligada com complexidade O linear pra procura. Vamos dar um exemplo besta de árvore assim.</p>
<p>Pense numa lista de números quaisquer como vinte e sete, quatorze, trinta e cinto, dez, dezenove, trinta e um e quarenta e dois. Agora vamos imaginar uma Struct Node com ponteiros left e right pra outros Nodes. E vamos desenhar como ficaria na memória a versão ineficiente, onde só vou preencher o ponteiro right de cada Node. Veja como elas ficam sequenciais como se fosse uma lista ligada.</p>
<p>É o pior caso. Mas a coisa só fica interessante quando falamos em árvores binárias balanceadas ou pelo menos semi-balanceadas. E nesse caso o tempo de procura vai ser proporcional ao height, ou altura da árvore em vez de ser proporcional ao total de elementos inseridos.</p>
<p>De exemplo, podemos desenhar a mesma sequência mais bonitinho, de uma forma balanceada pra ter ambos left e right preenchidos. Veja como fica mais bonito, com uma altura tamanho 2, sem contar a raiz, em vez de ter altura 7 que era a altura da lista ligada. Mas até aqui não ganhamos muita coisa a não ser reorganizar a estrutura. Se eu quiser procurar onde tá a chave 31 ainda ia precisar sair comparando quase todos os Nodes e a procura seria O linear do mesmo jeito que na lista ligada. Vamos ver como melhorar isso.</p>
<p>Eu falei antes que um dos problemas de um array é quando queremos inserir um elemento no meio dele. Digamos, inserir na posição 5 de um array de 10 elementos. Tem duas formas. A primeira é copiar os elementos da posição 6 até 10 uma posição mais pra frente - se ainda tiver espaços vazios) e inserir o novo elemento na posição 6. Outra forma é se for uma lista ordenada, daí inserimos o novo elemento no fim do array e mandamos ordenar ele usando um Timsort ou Quicksort da vida.</p>
<p>Qualquer uma das formas pode ter uma complexidade maior que O linear, provavelmente loglinear que é a complexidade de um algoritmo de ordenação como Quicksort. Mas, e se fosse possível inserir um elemento numa lista pré-ordenada em complexidade menor que linear, talvez logaritmica?</p>
<p>Apesar do nome árvore, e apesar da imagem na cabeça ser algo como diretórios e arquivos numa hierarquia, pense em árvores binárias como se fosse uma lista mesmo. Aqui vem o negócio que se fala sempre sobre abstrações. O objetivo é o seguinte: ter uma lista ordenada que, no geral, é mais barato de procurar e mais barato de inserir ou apagar elementos do que um array ou lista ligada.</p>
<p>Árvores aparecem porque, como expliquei no video anterior, se temos um problema que podemos ir particionando, tipo quebrando uma lista ao meio, depois quebrando essa metade ao meio, isso é característica de algo logarítmico. Pense numa árvore visualmente como se fosse uma lista particionada. A raíz poderia ser o elemento mais do meio da lista e cada um de seus filhos tem metade da lista e assim sucessivamente até o fim da lista.</p>
<p>Mas só particionar não tem muita vantagem se a lista estiver bagunçada, tudo fora de ordem. O grande truque é ter essa árvore com os itens ordenados. Porém, o que significa Nodes ordenados numa árvore que não parece ter uma única direção? Quem é o primeiro elemento? Quem é o segundo? Vamos pegar essa sequência de 14 números aleatórios e organizar numa árvore binária ordenada. Veja como o número 0 tá mais pra esquerda, o 34 virou a raíz e mais pra direita tem números maiores como 99.</p>
<p>O único código que vou mostrar linha a linha hoje é da binary search tree ou árvore de procura binária. Vamos começar criando a struct Node, parecido com o da lista ligada e do hashtable. É muito simples, tem um campo pro número que vamos guardar. Daí os ponteiros de left e right pros Nodes filhos. Agora vamos direto fazer uma função main que vai popular essa árvore. Note que eu declarei a variável root só com o tipo Node em vez de <code>struct Node</code> como vim fazendo antes. Isso porque eu declarei ele como um novo tipo com <code>typedef</code> então <code>Node</code> é a mesma coisa que <code>struct Node</code>. Fica mais curto de escrever e agora vocês já viram do outro jeito pra ver a vantagem de usar assim.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

typedef struct Node {
    int data;

    struct Node* left;
    struct Node* right;
} Node;

Node* insert(Node *root, int data) {
    Node *temp = (Node*) malloc(sizeof(Node));

    temp-&gt;data = data;
    temp-&gt;left = NULL;
    temp-&gt;right = NULL;

    if (root == NULL) {
        root = temp;

    } else {
        Node *current = root;
        Node *parent = NULL;

        while(1) {
            parent = current;

            if (data &lt; parent-&gt;data) {
                current = current-&gt;left;

                if (current == NULL) {
                    parent-&gt;left = temp;
                    return root;
                }
            } else {
                current = current-&gt;right;

                if (current == NULL) {
                    parent-&gt;right = temp;
                    return root;
                }
            }
        }
    }
    return root;
}

Node* search(Node *root, int data) {
    Node *current = root;
    printf(&#34;Visiting elements: &#34;);

    while(current) {
        printf(&#34;%d &#34;, current-&gt;data);

        if (data &lt; current-&gt;data) {
            current = current-&gt;left;
        } else if (data &gt; current-&gt;data) {
            current = current-&gt;right;
        } else {
            return current;
        }
    }
}

void pre_order_traversal(struct Node* root) {
    if(root) {
        printf(&#34;%d &#34;, root-&gt;data);
        pre_order_traversal(root-&gt;left);
        pre_order_traversal(root-&gt;right);
    }
}

void inorder_traversal(struct Node* root) {
    if(root) {
        inorder_traversal(root-&gt;left);
        printf(&#34;%d &#34;, root-&gt;data);
        inorder_traversal(root-&gt;right);
    }
}

void post_order_traversal(struct Node* root) {
    if(root) {
        post_order_traversal(root-&gt;left);
        post_order_traversal(root-&gt;right);
        printf(&#34;%d &#34;, root-&gt;data);
    }
}

int _print_t(Node *tree, int is_left, int offset, int depth, char s[20][255])
{
    char b[20];
    int width = 5;

    if (!tree) return 0;

    sprintf(b, &#34;(%03d)&#34;, tree-&gt;data);

    int left  = _print_t(tree-&gt;left,  1, offset,                depth &#43; 1, s);
    int right = _print_t(tree-&gt;right, 0, offset &#43; left &#43; width, depth &#43; 1, s);

    for (int i = 0; i &lt; width; i&#43;&#43;)
        s[2 * depth][offset &#43; left &#43; i] = b[i];

    if (depth &amp;&amp; is_left) {

        for (int i = 0; i &lt; width &#43; right; i&#43;&#43;)
            s[2 * depth - 1][offset &#43; left &#43; width/2 &#43; i] = &#39;-&#39;;

        s[2 * depth - 1][offset &#43; left &#43; width/2] = &#39;&#43;&#39;;
        s[2 * depth - 1][offset &#43; left &#43; width &#43; right &#43; width/2] = &#39;&#43;&#39;;

    } else if (depth &amp;&amp; !is_left) {

        for (int i = 0; i &lt; left &#43; width; i&#43;&#43;)
            s[2 * depth - 1][offset - width/2 &#43; i] = &#39;-&#39;;

        s[2 * depth - 1][offset &#43; left &#43; width/2] = &#39;&#43;&#39;;
        s[2 * depth - 1][offset - width/2 - 1] = &#39;&#43;&#39;;
    }

    return left &#43; width &#43; right;
}

void print_t(Node *tree)
{
    char s[20][255];
    for (int i = 0; i &lt; 20; i&#43;&#43;)
        sprintf(s[i], &#34;%80s&#34;, &#34; &#34;);

    _print_t(tree, 0, 0, 0, s);

    for (int i = 0; i &lt; 20; i&#43;&#43;)
        printf(&#34;%s\n&#34;, s[i]);
}

int main() {
    int i;
    int array[] = { 34, 84, 15, 0, 2, 99, 79, 9, 88, 89, 18, 31, 39 };
    Node *root = NULL;

    for(i = 0; i &lt; 17; i&#43;&#43;) {
        root = insert(root, array[i]);
        printf(&#34;Inseriu: %d\n&#34;, array[i]);
    }

    Node *temp = search(root, 30);

    if(temp) {
        printf(&#34;[%d] Element found.\n&#34;, temp-&gt;data);
    } else {
        printf(&#34;[x] Element (%d) not found.\n&#34;, i);
    }

    temp = search(root, 88);

    if(temp) {
        printf(&#34;[%d] Element found.\n&#34;, temp-&gt;data);
    } else {
        printf(&#34;[x] Element (%d) not found.\n&#34;, i);
    }

    printf(&#34;\nPreorder traversal: &#34;);
    pre_order_traversal(root);

    printf(&#34;\nInorder traversal: &#34;);
    inorder_traversal(root);

    printf(&#34;\nPost order traversal: &#34;);
    post_order_traversal(root);

    printf(&#34;\n&#34;);
    print_t(root);

    return 0;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Começamos com um array de 14 elementos, números aleatórios. E um Node chamado root pra ser a raíz. Fazemos um loop pra inserir todos os elementos desse array na árvore. Então já sabemos que vamos precisar de uma função chamada <code>insert</code>. Só pra ficar mais fácil de explicar e não precisar falar de ponteiro de ponteiros, a primeira variável vai ser o endereço pra raíz, que começa nulo e a função devolve o endereço da raíz de volta quando popular. Tem jeito melhor de fazer isso, mas pro exemplo isso é suficiente.</p>
<p>Chegou a hora de tentar procurar um elemento dentro da árvore criando outra função, a <code>search</code>. Mesma coisa dos outros episódios, vamos passar o endereço da raíz e o número que tamos procurando. Agora vamos voltar lá pro começo do arquivo e criamos essas funções, começando pela <code>insert</code> que vai receber um ponteiro pra raíz e o número que queremos inserir e retorna de volta o endereço da raíz.</p>
<p>Começamos criando uma instância de Node no heap com malloc e associando o endereço na variável <code>temp</code> de temporário. Daí preenchemos a estrutura com o número que passamos no campo <code>data</code> e inicializamos os ponteiros de left e right pra ser nulo. Aliás, importante, isso foi uma coisa que esqueci mesmo nos exemplos de lista ligada e hashtable. Se não inicializar os ponteiros com NULL, explicitamente, nada garante que no espaço alocado não havia bits de outra coisa lá e os campos podem começar já preenchidos com lixo, apontando pra algum lugar aleatório bizarro. Das duas uma, ou eu inicializo na mão como fiz agora gravando NULL ou em vez de <code>malloc</code> posso usar <code>calloc</code> pra que já faz essa limpeza.</p>
<p>Voltando, o começo é simples. Se a raiz, ou root, for nula como é o caso na primeira vez, ele passa a apontar pro mesmo lugar que a variável <code>temp</code>. Daí termina o <code>if</code> e dá return do novo root. Caso contrário, vamos fazer uma cláusula <code>else</code> e começamos criando um novo ponteiro temporário chamado <code>current</code> que aponta pro Node do root e outro ponteiro chamado <code>parent</code> de pai ou mãe, tanto faz, que começa nulo.</p>
<p>Começamos fazendo um loop infinito com <code>while(1)</code>. Se a gente não fizer nada dentro desse loop pra sair, vai ficar repetindo dentro do loop pra sempre. Dentro começamos fazendo a variável <code>parent</code> apontar pra <code>current</code> que, na primeira passada do loop é o primeiro Node que já tá inserido. O truque começa aqui.</p>
<p>Vamos checar dois casos, se o número que queremos inserir agora for menor que o número do parent. Se não for, cai no <code>else</code> que é o caso oposto, se o número que estamos inserindo for maior ou igual ao do Node parent. No nosso exemplo, a raíz é o número 34. O próximo número da lista é 84. Então ele cai no bloco de else.</p>
<p>Nesse bloco fazemos a variável current apontar pra direita do Node atual, do 34. E agora podemos ter dois casos. Se o Node current for nulo ou se já tiver outro Node. Nesse caso do 2o item ele ainda é nulo, então fazemos o ponteiro pra direita do node parent apontar pro temporário e saímos do loop dando return da raíz de novo.</p>
<p>Vamos completar o código com o próximo item da lista que é o 15. Vai ser um novo insert, passamos o root e o número 15, daí criamos um novo Node temp pra acomodar esse valor 15. Root não é nulo, então entramos no primeiro bloco <code>else</code>. Variável current aponta pro root, parent pra nulo. Entramos no <code>while</code> infinito. Agora fazemos a variável parent ser igual ao current porque vamos usar pra navegar pela árvore; e o parent sempre vai tá apontando pro Node imediatamente acima dela. Fica mais fácil de entender tendo esse diagrama na cabeça.</p>
<p>Agora checamos que 15 é menor que o valor no Node parent, que é raíz 34, então o current vai ser o espaço pra esquerda do Node atual. Por acaso, esse espaço ainda tá vazio, então vai passar a ser o Node 15 que acabamos de criar. Retornamos o root de novo e quebramos o loop infinito. E neste estágio temos uma lista com o 34 no topo, 15 na esquerda e 84 na direita. O código de inserção tá completo, vamos testar mais uma vez com o próximo número da lista, o zero.</p>
<p>Começamos no topo da função insert, passando a raiz e número 0. Criamos o node temporário que vai conter o zero, root não é nulo então seguimos criando o ponteiro current apontando pro Node 34 e parent nulo. Entra no loop infinito e começamos com o parent igual ao current que é a raiz.</p>
<p>Como zero é menor que o parent, que é o 34 ainda, fazemos o current ser o Node à sua esquerda, que é o 15. Sendo o current não nulo, então sai desse bloco e chega ao fim do while. Como o loop é infinito repetimos. Agora o parent vai ser o current 15. De novo, zero é menor que o atual parent que é 15 então fazemos o current ser o Node left à esquerda. Agora não tem ninguém lá, é nulo, e é onde vamos colocar o node zero. Pronto, atribuímos o left do parent 15 pra ser o node temporário zero e damos return pra sair do loop infinito.</p>
<p>Vou acelerar aqui do lado pra inserir os elementos que faltam, e a árvore vai ficando dessa forma. Obviamente não fica bonitinho assim na memória, mas cada Node tem ponteiros apontando pra até dois outros Nodes e podemos rearranjar num diagrama pra ficar visualmente assim. O importante é o seguinte: começa lendo do Node mais à esquerda e vai indo pra direita. Começa pelo zero, depois dois, nove, daí quinze, dezoito, trinta e um e chegamos na raíz trinta e quatro, continua pro trinta e nove, setenta e nove e vai indo até o noventa e nove. Notaram como a lista tá ordenada?</p>
<p>Antes de mostrar como fica a função search, acho que vale mostrar como pegar de volta a lista ordenada e mostrar na tela. Temos 3 formas de navegar por essa lista, pre order, post order e in order. Pre order começa da raiz e vai pra esquerda primeiro, depois pra direita. Indo de cima pra baixo. Post order começa do primeiro node mais à direita, no caso o nove e vai subindo, de baixo pra cima.</p>
<p>In order, como o nome diz é na ordem. E qualquer um desses casos é simples usando recursão. Primeiro vamos colocar na main a chamada pra função inorder_traversal passando o node raiz. Precisamos criar essa função. Ela começa checando se o node passado é nulo, se for, não faz nada e retorna. Caso contrário chama ela mesma passando o Node à esquerda.</p>
<p>Com os nossos valores de exemplo, se a gente pensar como ficaria a Stack do programa, vamos começar chamando inorder pro Node 34. Ela não é nula. Então chama ela mesma de novo passando o Node 15. Acumula no topo da Stack porque não retornamos ainda. Agora o node continua não sendo nulo, então chama ela de novo com o próximo node à esquerda, que é o do zero. Empilha na Stack, de novo. E quando chamarmos outra vez, o Node zero não é nulo, então passamos o node à esquerda dela que, agora é nulo. Empilhamos.</p>
<p>Agora a função vai receber nessa variável root um nulo, então o <code>if</code> vai ser verdadeiro, não faz nada e retorna. Desempilha da Stack. Voltamos pra chamada anterior que vai continuar do Node 0 e dar <code>printf</code> na tela. Vai imprimir zero e agora vamos chamar a função de novo passando quem tá à direita do Node 0, e tem o Node 2. Empilhamos.</p>
<p>Mesma coisa, prestem atenção, O node não é nulo, agora chamamos a função passando o ponteiro pro Node à esquerda do 2, por acaso é nulo. Empilhamos. Já sabemos que o <code>if</code> vai checar a variável como nulo, não faz nada e desempilha. Agora dá <code>printf</code> pro 2 e chama a função passando quem tá à direita do dois. Por acaso é o Node 9. Empilha.</p>
<p>Mesma coisa, node não é nulo, chamamos a função pro node à esquerda do nove que é nulo, já sabemos que vai empilhar, entrar e desempilhar porque é nulo, então vamos pular e na sequência vai dar <code>printf</code> do 9. Agora tenta chamar a função pro Node à direita do 9, que é nulo. De novo, vai empilhar, checar que é nulo, não faz nada e desempilha. E acaba a função pro 9, retorna e desempilha.</p>
<p>A função do 2 também termina, desempilha. A chamada pro zero também acaba e desempilha. Voltamos pro Node 15. Agora continua dando <code>printf</code> pra ele, imprime 15 na sequência do 9 e chama a função pro node à direita do 15, que vai ser o 18. Empilha. Node não é nulo então chama a função pro node à esquerda, que é nulo, podemos pular porque sabemos que vai empilhar e desempilhar. Continua dando <code>printf</code> pro 18. Agora chama a função pro Node 31 à direita.</p>
<p>Pausa, agora olha só, já imprimiu o zero, dois, nove, quinze, dezoito e o próximo vai ser 31, tudo na ordem crescente, ordenadinho e se continuar fazendo a mesma coisa vai imprimir até o 99 na ordem. Se você nunca viu isso antes deve ter dado um nó na cabeça. Mas é justamente isso que precisa acontecer, porque essa estrutura e esse jeito de operar não é intuitivo se você nunca estudou estruturas de dados. Por intuição você vai parar nas listas lineares e nunca vai entender o que vem depois, que são grafos.</p>
<p>Ao final, só de ter inserido um monte de números aleatórios na árvore binária conseguimos pegar os elementos já ordenados. O mesmo resultado poderia ser obtido inserindo os números num array e passando um Quicksort da vida neles. Mas entenda a diferença. Se for considerar puramente só inserção de novos valores, é mais rápido colocar num array, porque é complexidade O constante. Já na árvore vai ser O logaritmico. Ou seja, a inserção na árvore é mais lenta.</p>
<p>Mas a comparação é injusta. A complexidade de inserir elementos numa árvore de procura binária é O logaritmico porque ele já insere os elementos ordenados. Já no array é O constante pra inserir no fim porque não estamos ordenando. Se também quisermos o array ordenado a cada inserção, precisaria rodar um quicksort no mínimo uma vez no final, daí vai ficar mais caro que O loglinear.</p>
<p>Quando o array estiver todo ordenado, podemos pesquisar usando procura binária. Eu não vou mostrar código, mas é assim. O jeito ingênuo de procurar num array é fazer um loop e ir comparando elemento a elemento da esquerda pra direita até achar o 88. Você ia precisar de 10 operações de comparação pra chegar lá. Mas tem um jeito mais rápido que só funciona se a lista já tiver sido ordenada antes.</p>
<p>Você pega o tamanho do array e divide por dois, no caso vai ser 7. Daí pega o endereço do primeiro elemento e soma pelo tamanho de cada elemento multiplicado por sete, que é o elemento 39, mais ou menos no meio da lista. Agora compara, 88 é menor que 39? Não é, então já sabemos que não tá na metade à esquerda do 39 e não precisamos olhar nenhum deles. Só de fazer isso já eliminamos metade da lista que não precisamos nos preocupar.</p>
<p>Recursivamente fazemos a mesma coisa, à direita do 39 temos 6 elementos sobrando, dividido por 2 são 3, somado à posição 7 chegamos na posição 10, que é o meio da metade da direita. E vai ser o elemento 88 por acaso. Comparamos com o valor procurado 88 e já chegamos nele. Ou seja, com 2 operações de endereço pra chegar no meio do meio e 2 operações de comparação chegamos no 88. 4 operações no total, contra os 10 do jeito ingênuo, menos da metade do processamento. E isso porque essa lista é muito pequena. Quanto maior a lista maior vai ser a economia.</p>
<p>Por isso tem vantagem trabalhar com listas ordenadas. Porém, como disse antes, custa O n vezes log de n, loglinear pra ordenar um array. E dependendo do algoritmo, pode custar espaço na memória pra fazer essa ordenação. Um mergesort por exemplo, acho que era um que custava mais caro em espaço extra pra conseguir ordenar.</p>
<p>Lembram da animação que mostrei de mergesort no episódio anterior? Vamos ver de novo. Estão vendo que ele vai guardando valores aqui embaixo? Isso é o espaço temporário que o mergesort gasta pra conseguir ordenar a lista em cima. Então, não é só velocidade de processamento pra se preocupar como também espaço temporário de memória que se gasta. Agora pensa na árvore. Ela não custa muito espaço adicional pra ordenar, e custa literalmente O logaritmico, tudo bem que é pra cada elemento, mas a complexidade é uma ordem de grandeza mais rápida que qualquer ordenação de array.</p>
<p>E uma vez tendo a árvore ordenada, a procura é parecida com o que acabamos de fazer com procura binária de array. Vamos ver. Voltando ao exemplo, precisamos implementar a função <code>search</code>, que vai começar recebendo como argumento o Node raiz da árvore e o valor que estamos procurando, no caso o 88.</p>
<p>Criamos uma variável temporária chamada <code>current</code> apontando pro <code>root</code>. De novo criamos um loop com <code>while</code> que vai quebrar se o node current for nulo. E isso só vai acontecer se a gente navegar pela árvore e nenhum dos Nodes tiver o valor que procurado. Agora é a mesma coisa que na busca binária de array. A inserção garante que nossa árvore binária já tá ordenada como acabei de mostrar.</p>
<p>Vamos comparar o número 88 com o valor do current que é a raíz 34. Como não é menor, pula pro <code>else if</code>. Como é maior faz o <code>current</code> ser o Node 84. Repete o loop. 88 ainda não é menor que 84, pula pro <code>else if</code> de novo e faz current ser o node à direita, right, que é o 99. Repete o loop. Agora sim, 88 é menor que 99 então vamos fazer o current pegar o Node à esquerda de 99, o ponteiro left. Repete o loop. Agora o Node que achamos não é menor, não é maior, então finalmente cai no último <code>else</code> e pronto, achamos o 88.</p>
<p>Em quantidade de comparações é equiparável à busca binária de array ordenado, ordem de grandeza Big O logaritmico porque ambos os casos estamos particionando. Entenda a relação. Inserir elementos no fim de um array é rápido, O constante, caso tenha espaços vazios sobrando no final. Se não tiver, precisa criar um novo array maior e copiar os elementos da anterior pra ela. Complexidade O linear. Se quiser que a lista esteja ordenada pra conseguir procurar via busca binária, vai custar O loglinear de um quicksort da vida. No hashtable não se ordena porque a posição depende do hashing da chave. A procura vai ser O linear também.</p>
<p>Então a pesquisa numa árvore binária vai ser melhor do que num array, lista ligada ou hashtable. Tudo depende de pra que você vai usar a lista. Pra guardar os campos que vem num formulário simples de cadastro, qualquer lista serve, são poucos elementos. Mesmo na força bruta ingênua, a diferença é pouca. Agora, pra gerenciar coisas como um cache de banco de dados, você vai escolher alguma coisa melhor que um hashtable ou uma árvore binária.</p>
<p>Não chegamos no fim da história. Essa nossa árvore binária rudimentar é só a base. O caso ruim pra árvore binária, da forma como implementamos, é tentar adicionar uma lista de números consecutivos. Por exemplo, vamos começar de novo a partir de 99 e tentar inserir cem. Como é maior que 99, vai pra direita. Agora cento e um. Como é maior que 99, vai pro 100. Sendo maior que 100, vai pra direita. Cento e dois, mesma coisa, quando chegar no cento e um, vai pra direita. E se continuarmos adicionando, olha como ficou nossa árvore. Parece um galho seco esperando pra cair, ficou basicamente como uma lista ligada, o pior caso.</p>
<p>Pior caso porque, sendo praticamente uma lista ligada, a complexidade pra procurar deixou de ser O logarítmico e voltou a ser O linear. Ou seja, pra achar o último elemento, vai precisar comparar com todos os outros anteriores. Uma árvore assim é desbalanceada. A gente precisa balancear e distribuir em outros galhos dessa árvore.</p>
<p>O que vimos até aqui é chamado de árvore de procura binária ou binary search trees, cujo acrônimo é BST. O que costuma ensinar depois é como balancear essas BSTs e uma das formas de fazer isso é o que se chama de RBTrees ou Red Black Trees. Antes que alguém saia correndo pros comentários, vou falar sobre AVLs já já, calma aí.</p>
<p>Uma das razões de eu querer pular esse assunto é porque realmente não estou com vontade de explicar linha a linha de como se faz inserções numa árvore Red Black. É muito detalhezinho, vai dar um puta trampo pra editar e indo linha a linha num video vai ser maçante pra caramba. Então, recomendo que vocês procurem o código em qualquer linguagem e testem linha a linha na máquina de vocês. Em vez disso, vou fazer diferente aqui.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include&lt;stdio.h&gt;
#include&lt;stdlib.h&gt;

#define RED   &#39;R&#39;
#define BLACK &#39;B&#39;

#define COUNT 10

typedef struct Node {
    int          key;
    char         color;
    struct Node *left;
    struct Node *right;
    struct Node *parent;
} Node;

// Based on CLRS algorithm, use T_Nil as a sentinel to simplify code
struct Node  T_Nil_Node;
Node* T_Nil = &amp;T_Nil_Node;

Node* Root = NULL;

// A utility function to create a new BST node
Node* newNode(int key)
{
    Node *temp   = (Node*) malloc(sizeof(Node));
    temp-&gt;key    = key;
    temp-&gt;color  = RED;
    temp-&gt;left   = NULL;
    temp-&gt;right  = NULL;
    temp-&gt;parent = NULL;

    return temp;
}

void rotateLeft( Node** T, Node* x)
{
    Node *y  = x-&gt;right;    // set y
    x-&gt;right = y-&gt;left;     // turn y&#39;s left subtree into x&#39;s right subtree{
    if (y-&gt;left != T_Nil)
        y-&gt;left-&gt;parent = x;
    y-&gt;parent = x-&gt;parent;  // link x&#39;s parent to y
    if (x-&gt;parent == T_Nil)
        *T = y;
    else if (x == x-&gt;parent-&gt;left)
        x-&gt;parent-&gt;left = y;
    else
        x-&gt;parent-&gt;right = y;
    y-&gt;left   = x;            // put x on y&#39;s left
    x-&gt;parent = y;
}

void rotateRight(Node** T, Node* y)
{
    Node *x  = y-&gt;left;     // set x
    y-&gt;left  = x-&gt;right;    // turn x&#39;s right subtree into y&#39;s left subtree{
    if (x-&gt;right != T_Nil)
        x-&gt;right-&gt;parent = y;
    x-&gt;parent = y-&gt;parent;  // link y&#39;s parent to x
    if (y-&gt;parent == T_Nil)
        *T = x;
    else if (y == y-&gt;parent-&gt;right)
        y-&gt;parent-&gt;right = x;
    else
        y-&gt;parent-&gt;left  = x;
    x-&gt;right  = y;         // put y on x&#39;s right
    y-&gt;parent = x;
}

void redBlackInsertFixup(Node** Root, Node* New)
{
    Node* temp;
    while (New-&gt;parent-&gt;color == RED)
    {
        if (New-&gt;parent == New-&gt;parent-&gt;parent-&gt;left)
        {
            temp = New-&gt;parent-&gt;parent-&gt;right;
            if (temp-&gt;color == RED)
            {
                temp-&gt;color = BLACK;
                New-&gt;parent-&gt;color = BLACK;
                New-&gt;parent-&gt;parent-&gt;color = RED;
                New = New-&gt;parent-&gt;parent;
            }
            else {
                if (New == New-&gt;parent-&gt;right)
                {
                    New = New-&gt;parent;
                    rotateLeft(Root, New);
                }
                New-&gt;parent-&gt;color = BLACK;
                New-&gt;parent-&gt;parent-&gt;color = RED;
                rotateRight(Root, New-&gt;parent-&gt;parent);
            }
        }
        else
        {
            temp = New-&gt;parent-&gt;parent-&gt;left;
            if (temp-&gt;color == RED)
            {
                New-&gt;parent-&gt;color = BLACK;
                New-&gt;color = BLACK;
                New-&gt;parent-&gt;parent-&gt;color = RED;
                New = New-&gt;parent-&gt;parent;
            }
            else {
                if (New == New-&gt;parent-&gt;left)
                {
                    New = New-&gt;parent;
                    rotateRight(Root, New);
                }
                New-&gt;parent-&gt;color = BLACK;
                New-&gt;parent-&gt;parent-&gt;color = RED;
                rotateLeft(Root, New-&gt;parent-&gt;parent);
            }
        }
    }
    Root[0]-&gt;color = BLACK;
}
void redBlackInsert(Node** T, int key)
{
    Node* z =  newNode(key);
    Node* y =  T_Nil;
    Node* x = *T;

    // Find where to Insert new node Z into the binary search tree
    while (x != T_Nil) {
        y = x;
        if (z-&gt;key &lt; x-&gt;key)
            x = x-&gt;left;
        else
            x = x-&gt;right;
    }

    z-&gt;parent = y;
    if (y == T_Nil)
        *T = z;
    else if (z-&gt;key &lt; y-&gt;key)
        y-&gt;left  = z;
    else
        y-&gt;right = z;

    // Init z as a red leaf
    z-&gt;left  = T_Nil;
    z-&gt;right = T_Nil;
    z-&gt;color = RED;

    // Ensure the Red-Black property is maintained
    redBlackInsertFixup(T, z);
}

#define MAX(a,b) (((a)&gt;(b))?(a):(b))

int height(Node* Root)
{
    int h = 0;

    if (Root != NULL) {
        if (Root == T_Nil)
            h = 1;
        else
        {
            int leftHeight  = height(Root-&gt;left);
            int rightHeight = height(Root-&gt;right);
            h = MAX(leftHeight, rightHeight) &#43; 1;
        }
    }

    return h;
}

void printTree(Node* root)
{
    if (root-&gt;left != T_Nil)
        printTree(root-&gt;left);
    printf(&#34;%d &#34;, root-&gt;key);
    if (root-&gt;right != T_Nil)
        printTree(root-&gt;right);
}

void padding ( char ch, int n ){
  int i;
  
  for ( i = 0; i &lt; n; i&#43;&#43; )
    putchar ( ch );
}

void structure ( struct Node *root, int level ){
  int i;
  
  if ( root == NULL ) {
    padding ( &#39;\t&#39;, level );
    puts ( &#34;~&#34; );
  } else {
    structure ( root-&gt;right, level &#43; 1 );
    padding ( &#39;\t&#39;, level );
    if(root-&gt;color) {
        printf ( &#34;%d,%c\n&#34;, root-&gt;key, root-&gt;color );
    } else {
        printf ( &#34;%d,N\n&#34;, root-&gt;key );
    }
    structure ( root-&gt;left, level &#43; 1 );
  }
}

int main(int argc, char* argv[])
{
    Node* Root = T_Nil;
    int list[14] = { 34, 84, 15, 0, 2, 99, 79, 9, 88, 89, 18, 31, 39 };
    //int list[10] = {1, 3, 2, 5, 4, 7, 6, 9, 8, 10};
    for (int i = 0; i &lt; 14; i&#43;&#43;)
    {
        //printf(&#34;%d &#34;, list[i]);
        redBlackInsert(&amp;Root, list[i]);
        printf(&#34;Inserindo %d\n&#34;, list[i]);
        structure(Root, 0);
    }
    printTree(Root);

    structure(Root, 0);
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O que muda numa árvore binária normal e numa Red Black? Pra começar o Node da árvore vai conter um campo extra de um bit, que pode ser zero ou um. Tanto faz se você chama zero de Red e um de Black ou vice versa, mas uma regra inviolável é que o Node raiz é sempre preto.</p>
<p>Por padrão, todo Node novo que formos criando vai começar sendo da cor vermelha. Mas depois de inserido na árvore precisamos checar a cor do pai e dos filhos, porque a regra é que um Node vermelho só pode ter pais e filhos pretos. Se depois de inserir não estiver assim, precisamos repintar os Nodes pra encaixar na regra. Parece estranho mas você vai entender.</p>
<p>Além disso, todo Node no fim da árvore, que você pode chamar de folhas ou leafs, e terminaria com os ponteiros left e right sendo nulo, vão apontar pra um Node de serviço que muitos nomeariam como Null Node ou TNil. Você pode chamar como quiser, mas é um Node que serve só pros ponteiros left e right dos últimos Nodes não serem nulo e seguir a regra das cores. Como é tudo ponteiro, todo mundo no final vai só apontar pra esse Node mas no diagrama, pra ficar mais bonito, vai parecer que cada um tá apontando pra um Node extra. Só lembrem que é tudo o mesmo Node TNil no final.</p>
<p>Uma das razões de ter esses Nodes no final é a regra que se contarmos todos os nodes entre a raíz e esse Nulo deve sempre ter o mesmo número de Node pretos. Na prática isso só vai acontecer se a árvore tiver a altura balanceada. Pra entender isso de balanço considere nossa árvore binária de exemplo. Conseguem ver como alguns galhos pendem mais pra um lado só? Os nodes 0 ou 15 pendem tudo pra direita. Do node 84 pende tudo pra esquerda.</p>
<p>Pra árvore ficar balanceada, precisamos fazer uma manutenção, movendo ponteiros de lugar durante a inserção. Lembra aquela ideia de fazer swap de ponteiro que eu falei no episódio anterior? É parecido aqui. Swaps ou trocas, são operações rápidas. Só muda uns dois ou três ponteiros de lugar, independente da quantidade total de Nodes, então é uma operação de complexidade O 1, constante.</p>
<p>Essa operação em árvores não é exatamente um swap, mas sim uma rotação. E isso tudo dito, vamos dar um exemplo visual com a mesma lista de números do exemplo, mas agora inserindo com o algoritmo de inserção de uma árvore Red Black. O 34 é a raíz, por isso ela é preta por padrão e seus ponteiros de left e right vão apontar praquele Node Nulo TNil. O próximo número é 84, que por padrão entra vermelho e igual na árvore binária vai pra direita porque é maior que 34. O próximo número 15 é menor que 34 então vai pra esquerda e também é vermelho por padrão.</p>
<p>Até aqui tudo bem, mas agora vamos inserir o número 0. Pelas mesmas regras, é menor que 34, menor que 15 então vai indo pra esquerda. E inserimos o Node zero vermelho. Mas a regra é que um node vermelho não pode ter um pai vermelho como o Node 15 em cima dele. Aqui começa o trabalho de zeladoria recolorindo o Node 15 e o Node 84 pra preto. Agora a regra foi satisfeita.</p>
<p>Sobre o caminho mais curto que falei, olha só. Quantos nodes pretos tem entre o 34 e o Nil embaixo do 0? 34 é preto, 1. 15 é preto, 2. 0 é vermelho, continua 2, e nil é preto, 3. Conseguem ver que qualquer outro caminho continua sendo 3 nodes pretos? Então as regras continuam satisfeitas. Vamos continuar tentando inserir o número 2.</p>
<p>E aqui a coisa ficou diferente. Olha o que aconteceu. Tentamos inserir o número 2. Pela regra da árvore binária, é menor que 34 então vai pra esquerda. É maior que zero então vai pra direita. Agora a árvore ficou desbalanceada. Por que? Pra começar temos dois Node vermelhos um atrás do outro. E o irmão do Node 0, que é o ponteiro pra direita do Node 15 tá vazio. Isso não pode acontecer, a ideia é que os nodes mais pra cima tenham left e right preenchidos, evitando ponteiros nulos antes de chegar nas folhas. Precisamos fazer a tal rotação.</p>
<p>Primeiro rotacionamos pra esquerda, subimos o Node 2 na árvore e descemos o Node 0 pra esquerda. Depois rotacionamos pra direita. Subimos o Node 2 pro mesmo andar da 15 e descemos o 15 pra direita. Finalmente repintamos o Node 2 e o Node 15 e pronto, voltamos a cumprir todas as regras do jogo. Nenhum Node vermelho tem pai ou filhos vermelhos e todos os caminhos do Node 34 até os nulos continua tendo o mesmo número de Node pretos, no caso 3. Vamos continuar.</p>
<p>Os números noventa e nove, setenta e nove e oitenta e oito vão sendo inseridos do mesmo jeito que na árvore binária. Mas agora vamos ver o 89. Mesma coisa, é maior que 34, vai pra direita. É maior que 84 vai pra direita. É menor que 89, vai pra esquerda e, sendo maior que 88, vai pra sua direita. Alerta! Temos dois nodes vermelhos um atrás do outro e o irmão do 88, ou seja o ponteiro pra direita do 89 tá vazio. Hora de rotacionar.</p>
<p>Começa rotacionando pra esquerda, daí o 89 passa a ser o pai de 88 e filho de 99. Rotacionamos pra direita e 99 passa a ser filho de 89 e irmão do 88. Vamos repintar o 88 e 89 e agora estamos cumprindo todas as regras de novo. Conseguem ver que se compararmos com a árvore binária de antes, agora parece visualmente mais balanceada? Ou seja, todos os Nodes antes do andar mais de baixo tem ambos os ponteiros left e right preenchidos, sem buracos sobrando.</p>
<p>Vamos continuar os outros números até o fim da lista sem parar mais. Continuamos com dezoito, trinta e um e trinta e nove. Daí escondemos os Nodes nulos no final pra ficar mais fácil de comparar. Olhem como a árvore Red Black e a árvore binária ficaram com um layout bem diferente. É assim que uma árvore binária balanceada se parece usando o método de Red Black.</p>
<p>Em termos de espaço extra sendo usado, é barato. Só um bit a mais pra cada Node, fora os ponteiros pro Node nulo no final de cada folha. E mesmo esse espaço tem como economizar e vou falar disso já já. O tempo de inserção é definitivamente mais caro, mas ainda assim na ordem de grandeza de O logaritmico.</p>
<p>Ou seja, em comparação com a árvore de procura binária, tá um pouco mais lento, mas a ordem de grandeza de complexidade não ficou muito mais caro. Procurar por itens nessa árvore é a mesma coisa da árvore de procura binária. Uma busca binária. E pra apagar vai ser um pouco mais lento também. Assim como na inserção precisa manter a árvore balanceada, então toda vez precisa checar se precisa rotacionar e recolorir.</p>
<p>Como eu prometi, ainda temos árvores AVL pra comentar, que chama assim por causa dos autores Adelson-Velsky e EM Landis, de 1962. Em resumo, diferente do sistema de bit de cor do Red Black, cada Node num AVL tem um número de balanço que pode ser menos um, zero ou um, pra árvore ser balanceada. Se for mais que menos dois ou dois, a árvore tá desbalanceada e precisa rotacionar e recalcular o balanço.</p>
<p>Embora ambos AVL e Red Black tenham operações com complexidade na ordem de grandeza O logaritmico, a procura numa árvore AVL é um pouco mais rápida, porque a Red Black ainda pode acabar menos balanceada que a AVL. A AVL é mais agressiva em manter a árvore estritamente balanceada. A inserção da AVL acaba sendo um pouco mais cara que a Red Black pra atingir isso. É sempre um trade-off. Se você fizer mais inserções do que procuras, um Red Black é melhor. Se o tempo de inserção não for importante, mas pra você o tempo de procura for muito importante, então talvez AVLs sejam melhores.</p>
<p>Mais do que isso, diferente dos Nodes de Red Black que são só 1 bit, os nodes de AVL precisam de dois bits extras, então vai ocupar um pouco mais espaço na memória. Além disso, os Blacks tem outra vantagem dependendo da implementação. Lembram quando falei sobre números inteiros com sinal e sem sinal? Se a gente souber que os números que vão ser armazenados na árvore sempre é positivo, podemos usar o primeiro bit, que normalmente seria pro sinal pra ser a cor do Node, daí economizamos esse bit a mais na estrutura de Node.</p>
<p>E só pra piorar o caso do AVL, eu disse antes que as rotações de Red Black são simples, complexidade O 1 constante. Mas as inserções de AVL podem ter mais rotações mais complicadas, daí o tempo varia mais. Por tudo isso, na dúvida, a escolha padrão acaba sendo Red Black, e AVL em casos especiais que você sabe que precisa das propriedades específicas dele.</p>
<p>Vou deixar linkado na descrição abaixo um documento do site da kernel do Linux que explica mais sobre como árvores Red Black são usados no Kernel. Parafraseando, os schedulers de I/O deadline e CFQ usam rbtrees pra rastrear requisições. RBTree você lembra que são Red Black Trees. O pacote de CD e DVD faz o mesmo. O código de timer de alta resolução usa rbtrees. O filesystem ext3 rastreia diretórios em uma rbtree. VMAs ou áreas de memória virtual usam rbtrees.</p>
<p>Seu Node.js e todo servidor com I/O assíncrono, por baixo usa o epoll do Linux. Rastreamento de descritores de arquivos, chaves criptográficas e scheduler de pacotes de rede usam rbtrees. Só pra comparar, lembram nosso código de exemplo de procura numa árvore binária? Vamos ver como essa documentação do site da kernel sugere fazer. E olha só, é bem parecido né? As mesmas comparações pra esquerda, pra direita ou quando encontra o Node. Meu exemplo foi rudimentar mas a estrutura básica é a mesma nos casos mais complexos.</p>
<p>Não sei porque as linguagens mais populares interpretadas não tem árvores Red Black por padrão. Eu diria que elas deveriam, porque assim como outras estruturas de dados, seriam melhor implementadas em C ou C++ do que em alto nível como Javascript ou Python. A classe <code>std::map</code> do C++ implementa árvores Red Black, assim como as classes TreeSet ou TreeMap do Java. Se for uma linguagem compilada como Go ou Rust, você pode baixar do GitHub alguma uma implementação que funciona em boa velocidade porque são compiladas. Mas tirando Java e C++ não sei de nenhuma outra que já vem embutida. Se alguém souber mande nos comentários.</p>
<p>Mas lembrem-se, árvores Red Black e AVL tem como objetivo chegar numa árvore de procura binária, como no nosso exemplo anterior, mas balanceadas. Os algoritmos de inserção que usam o sistema de cores ou balanço e rotação servem pra nos dar uma árvore balanceada que vai oferecer o menor tempo médio de procura binária. Mas o que vimos até agora continua sendo uma árvore de procura binária, com Nodes que só tem um valor e dois ponteiros, um pra esquerda e outro pra direita.</p>
<p>Depois disso ainda existe a B-Tree que é uma estrutura de árvore que também é auto-balanceada, ou seja, durante a inserção e remoção de Nodes ele faz a zeladoria de se rebalancear. Mas em vez de Nodes que só tem dois filhos, num B-Tree os Nodes podem ser X filhos maiores que 2. E mais coisas que não vou detalhar aqui. A definição vem do livro do Knuth e todo mundo usa a mesma, então pesquisem sobre B-Trees como lição de casa. Visualmente é algo como nessa imagem do lado. Ainda uma árvore, idealmente balanceada, mas com mais filhos por Nodes.</p>
<p>E indo mais longe chegamos na B PLUS Trees, que você pode pensar como B-Trees mas onde os Nodes não contém os valores em si. Em vez disso, aponta pra folhas, e esse nodes folha é que vão ter os valores que você quer armazenar. Os Nodes pais vão ter só as chaves que identificam esses valores. E é essa árvore que você vai ver em tudo que usa armazenamento orientado a blocos.</p>
<p>Eu tô pensando em fazer um episódio falando sobre blocos de armazenamento. É por isso que se você já brincou com AWS ou Google Cloud ou Azure, já deve ter visto o termo “block storage” pra identificar HDs virtuais. Por agora só entenda que é isso que faz B+ Trees serem ideais pra coisas como filesystems e bancos de dados. Tudo que você imaginar que armazena dados em disco provavelmente é uma B+ Tree ou derivação. Exemplo. Os filesystem de Linux como ReiserFS, EXT4. O NTFS do Windows. O APFS dos Macs. A maioria dos bancos de dados relacionais como DB2, SQL Server, Oracle, SQLite e por aí vai. É tudo B+ Trees.</p>
<p>Falando em bancos de dados. Pra entender como os dados são armazenados em disco, você precisa entender B+ Trees. Se você já usou qualquer MySQL ou Postgres da vida vai lembrar que tem a boa prática de sempre criar índices pra melhorar o tempo das suas pesquisas. E se você leu stackoverflow o suficiente também vai lembrar que povo fala pra não sair criando índice pra tudo que é lado, como se não houvesse amanhã, senão vai acabar com a performance. E aí você fica sem saber o que fazer, cria ou não cria índices então?</p>
<p>Se você entendeu a ideia de pegar uma lista e colocar numa árvore de procura binária balanceada e a idéia de hashing da hashtable do episódio anterior, meio que já entendeu uma parte da charada. O que são registros de um banco? Pense como se fosse uma struct de C, como as que usamos em todos os exemplos até agora. Agora pense em gravar os bits de instâncias de structs no disco em vez da memória. Uma tabela de banco de dados grande pode ter milhares ou milhões de registros. Seria bem lento sair procurando registro a registro como numa lista ligada. Mesmo se for uma árvore.</p>
<p>Diferente de ler da memória, pense que num disco mecânico você precisa sair caçando bits em trilhas num disco. E ficar caçando endereços assim ia ser bem demorado. Mas se não tem nenhum índice, é isso que vai acontecer mesmo. Pelo menos fazendo uma busca binária, você vai eliminando uma boa parte de registros que não precisa vasculhar, mas em milhões de registros, mesmo com busca binária vai ser devagar. É o que muitos já devem ter visto nos seus bancos de dados favoritos quanto a pesquisa cai num table scan.</p>
<p>Quando você procura por chave primária ou por um campo indexado é bem mais rápido. Pense num índice como uma segunda árvore, uma B-tree que é a tal árvore onde cada Node pode ter vários filhos, e é auto-balanceada, ou seja, inserções e deleções re-balanceiam a árvore via operações como rotação. Ela fica mais compacta porque é como se fosse uma cópia da sua tabela só que com os campos indexados.</p>
<p>Imagina num exemplo simples de árvore de procura binária, como esse na imagem aqui do lado. Se eu fizer uma pesquisa pra achar todos os salários maiores que 10 mil, depois de eliminar a metade esquerda da árvore. Agora temos os IDs ou endereços de onde esses registros estão na outra árvore no disco, e podemos buscar só elas.</p>
<p>E porque não é bom criar um montão de índices? Pense em inserir uma tabela como inserir numa árvore Red Black. Pra manter a árvore balanceada, pra acelerar pesquisas, você precisa repintar e rotacionar toda hora. Cada índice da sua tabela é como se fosse uma árvore dessas. Quanto mais índices tiver, mais caro fica inserir em várias delas, porque você precisa inserir na tabela propriamente dita e depois atualizar cada um dos índices que você criou. Índices são bons pra acelerar as pesquisas, mas o custo é aumentar o preço de operações de inserts, updates e deletes.</p>
<p>De qualquer forma o objetivo de hoje não é detalhar como bancos de dados funcionam, mesmo porque eu mesmo não sei tudo de cabeça Mas só de saber que seu sistema de arquivos, seu banco de dados usam uma implementação de B+ Trees e que os índices que tornam suas pesquisas mais rápidas e suas inserções mais lentas, provavelmente é uma implementação de B-Trees, já vai ajudar você a procurar mais material pra estudar.</p>
<p>Uma última coisa é não confundir B-Trees com Binary Trees ou árvores binárias. Esse B é de Balanced Trees, árvores balanceadas. E balancear árvores tem um custo, as tais operações de rotação por exemplo. Mas no fim do dia, embora você pense visualmente numa árvore como uma hierarquia, na verdade é uma lista ordenada, como um array ou uma lista ligada, mas estruturada de forma diferente. De forma não linear.</p>
<p>A complexidade de cada tipo de algoritmo em cada tipo de estrutura de dados tem complexidades diferentes de processamento e uso de recursos. Alguns são rápidos mas consomem mais memória. Outros são mais devagar na inserção mas mais rápidos na procura. Casos excepcionais podem fazer um algoritmo O logaritmico cair pra O linear ou até mesmo O loglinear.</p>
<p>Outras coisas que você deve prestar atenção. Se você partir do zero, de C, sozinho, é possível que compreenda um pouco sobre as complexidades de manipular e procurar elementos num array. Com um pouco de prática, vai naturalmente chegar numa lista ligada. Acho difícil chegar num Quicksort sozinho, mas se por acaso esbarrar em algum texto sobre particionamento e logaritmos, talvez chegue em algo parecido pra ordenação e busca binária. John Von Neumann chegou no mergesort.</p>
<p>Se você se esforçar bastante, talvez consiga evoluir da lista ligada e array pra hashtable e árvore de procura binária desbalanceada que vimos hoje. Mas eu acho muito improvável você sozinho, sem a ajuda de nenhum livro, chegar na idéia de árvore balanceada e mecanismos como Red Black, AVL ou mesmo B-Trees. Pra isso você precisa de mais literatura. Especialmente porque conceitos como colorização e rotação não são intuitivos, mesmo se você já tiver a idéia de swap de variáveis na cabeça.</p>
<p>A ideia de colorização não é usada só em árvores Red Black. Um dos mecanismos de Garbage Collectors se chama TriColor Marking, onde a fase de marcação de objetos categoriza quem pode ou quem não pode ser descartado usando um sistema de três cores, preto, branco e cinza. Se você entendeu a ideia do vermelho e preto, já tem uma noção de TriColor marking. Talvez eu fale sobre isso se for explicar como funcionam os garbage collectors em alto nível , mas por hoje fica a dica.</p>
<p>Eu espero que pelo menos tenha conseguido fazer vocês que são iniciantes enxergarem um pouco mais de como as coisas funcionam por baixo do seu Javascript e Python. Mesmo que não usem diretamente no trabalho, reservem um tempinho pra estudar o básico de C. Especialmente porque se vocês se empolgarem, C e C++ vão ser úteis caso um dia queiram usar uma biblioteca em C dentro do Javascript. E sim, isso é possível e é como muitas bibliotecas são feitas de verdade.</p>
<p>O nosso linguição de bits sozinho é só isso, bits um atrás do outro. Mas quando definimos estruturas de dados, começamos a dar forma a pedaços desses bits. Delimitamos essas estruturas pelo tamanho de cada primitiva, como inteiros de 32 ou 64 bits, e os endereços em que elas se localizam no linguição. Os bits sozinhos não significam nada. Só quando nós programadores definimos o que cada pedaço é que ela começa a ter sentido. Por exemplo, uma sequência de inteiros um atrás do outro pode ser um array só de números mesmo ou de letras, formando um String, quem decide qual é qual sou eu, programador.</p>
<p>Um pacote de bits que tem codificado um número que é outra posição no linguição, é um Node. E esses Nodes podem ser organizados como listas ligadas, hashtables ou árvores. Daí vem o problema de navegar por esses pacotinhos de bits. O jeito simples é sequencialmente como num array, mas logo a gente vê que isso é limitado. Daí saímos pulando de endereço pra endereço de Nodes. Mas isso é lento e complexo. Aí temos que inventar formas de minimizar esse trabalho e chegamos em algoritmos como de ordenação e procura.</p>
<p>E à medida que evoluímos o conceito, uma árvore desbalanceada se torna balanceada. Uma procura que levava tempo linear vira logaritmico. E uma estrutura que era simples, vai ficando mais sofisticada. Saímos de heaps de memória até índices de bancos de dados com a mesma categoria de estruturas e algoritmos. O importante é: mesmo se você treinasse por anos a fio, sem estudar a fundação, nunca vai chegar no que vimos nesses últimos 3 episódios sozinho. E ia continuar fazendo errado sem saber porque. “Meu programa tá lento, deve ser meu CPU que é velho”. E esquece que anos atrás se fazia programas que rodam mais rápido que o seu em CPU ainda mais velho. Então o problema não é o hardware não ser o suficiente, mas sim você não saber usar o máximo que ele pode oferecer e se limitar a se conformar com “ah, pelo menos roda”.</p>
<p>Tudo que falei até aqui é o básico. Cada assunto que falei tem bem mais detalhes por trás. E mesmo quando você tiver estudado tudo, ainda assim não vai fazer programas bons. Mas ao longo do seu treino e estudos, as peças vão começar a se encaixar mais rápido e você vai começar a dar saltos maiores do que quem começou junto com você mas preferiu pular essa teoria toda. Peguem o livro do Cormen, Tanenbaum e estudem. É chato pra caramba, mas quanto mais cedo fizer isso, melhor vai ser depois.</p>
<p>E com isso, finalmente cheguei no final deste assunto. Como eu disse no começo, assistam esta série múltiplas vezes. Se você é iniciante, não vai entender vendo só uma vez e nem tem tudo aquia. Precisa pesquisar mais sobre os assuntos que falei, fazer você mesmo esses códigos como exercício e só assim começar a apreciar como as coisas funcionam. Se acharem algum erro no video não deixem de mandar nos comentários abaixo, se curtiram o video deixem um joinha, assinem o canal e cliquem no sininho pra não perder os próximos episódios. Compartilhem o video com seus amigos pra ajudar o canal. A gente se vê, até mais.</p>
]]></content:encoded><category>algoritmos-e-estruturas-de-dados</category><category>ciencia-da-computacao</category><category>bancos-de-dados</category><category>akitando</category></item><item><title>[Akitando] #94 - O que vem DEPOIS do Hello World | Consertando meu C</title><link>https://akitaonrails.github.io/2021/03/22/akitando-94-o-que-vem-depois-do-hello-world-consertando-meu-c/</link><guid isPermaLink="true">https://akitaonrails.github.io/2021/03/22/akitando-94-o-que-vem-depois-do-hello-world-consertando-meu-c/</guid><pubDate>Mon, 22 Mar 2021 15:12:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/YyWMN_0g3BQ"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Algoritmos e Estruturas de Dados é parte do conhecimento fundamental que todo programador precisa saber ou nunca vai avançar de verdade na profissão. Vamos ver a ponta do iceberg pra vocês terem uma idéia do que isso significa. E vou aproveitar pra consertar alguns erros que cometi no episódio anterior.&lt;/p&gt;
&lt;p&gt;Conteúdo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;01:47 - Consertando meus erros de C&lt;/li&gt;
&lt;li&gt;04:13 - Segmentos da Memória Virtual&lt;/li&gt;
&lt;li&gt;11:53 - Arrays de Javascript são &amp;ldquo;Arrays&amp;rdquo;?&lt;/li&gt;
&lt;li&gt;15:56 - Lista Ligada em C&lt;/li&gt;
&lt;li&gt;21:37 - Hashtable em C&lt;/li&gt;
&lt;li&gt;39:11 - Algoritmos de Ordenação&lt;/li&gt;
&lt;li&gt;42:11 - Complexidade e Big O&lt;/li&gt;
&lt;li&gt;43:30 - Vendedor Viajante e Fatorial&lt;/li&gt;
&lt;li&gt;47:29 - Mergesort vs Quicksort&lt;/li&gt;
&lt;li&gt;52:18 - Melhor e pior caso, Bubble vs Quick&lt;/li&gt;
&lt;li&gt;55:45 - Livros sobre Algoritmos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Links:&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/YyWMN_0g3BQ"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Algoritmos e Estruturas de Dados é parte do conhecimento fundamental que todo programador precisa saber ou nunca vai avançar de verdade na profissão. Vamos ver a ponta do iceberg pra vocês terem uma idéia do que isso significa. E vou aproveitar pra consertar alguns erros que cometi no episódio anterior.</p>
<p>Conteúdo:</p>
<ul>
<li>00:00 - Intro</li>
<li>01:47 - Consertando meus erros de C</li>
<li>04:13 - Segmentos da Memória Virtual</li>
<li>11:53 - Arrays de Javascript são &ldquo;Arrays&rdquo;?</li>
<li>15:56 - Lista Ligada em C</li>
<li>21:37 - Hashtable em C</li>
<li>39:11 - Algoritmos de Ordenação</li>
<li>42:11 - Complexidade e Big O</li>
<li>43:30 - Vendedor Viajante e Fatorial</li>
<li>47:29 - Mergesort vs Quicksort</li>
<li>52:18 - Melhor e pior caso, Bubble vs Quick</li>
<li>55:45 - Livros sobre Algoritmos</li>
</ul>
<p>Links:</p>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>No último episódio eu fiz diversos tipos de Hello World em C pra explicar o básico de programação mais de baixo nível, e foi super trabalhoso porque eu não queria quebrar o  conteúdo em vários episódios.</p>
<p>Eu não programo em C faz muitos anos então muita coisa enferrujou mesmo. Eu nunca fui um bom programador de C e agora então, só o básico. Eu editei aquele script algumas vezes e mudei os exemplos enquanto fui revisando, e tirei muita coisa de cabeça. Nisso acabei deixando passar vários erros que me deixaram bem frustrado porque eu só vi depois. O bom foi que vários de vocês viram e relataram nos comentários. Eu coloquei algumas na erratas daquele vídeo e vou usar o video de hoje pra complementar o que eu acho que faltou falar.</p>
<p>A ideia destes vídeos não é ser um curso de C, nem de longe. É a mesma coisa na faculdade, C ou Pascal era usado só pra não termos que usar alguma pseudo-linguagem pra ensinar os conceitos. Tem várias nomenclaturas e conceitos que aos olhos de quem programa C no dia a dia ficou feio. Muita coisa eu simplifiquei pra explicação ficar mais fácil. Pra aprender C de verdade tem que ver livros com The C Programming Language do Kernighan e Ritchie, os criadores da linguagem e estudar muito código fonte.</p>
<p>Se você é programador de C, perdeu uma oportunidade. Podia ter feito um video chamado “Vamos consertar o C cagado do Akita”. É um bom tema. Tem muito pouco video sobre C hoje em dia. De qualquer forma, já que ninguém fez, vou fazer eu mesmo. A primeira metade de hoje vai ser uma errata aproveitando pra adicionar mais conteúdo ao que eu já tinha explicado e a segunda metade vai molhar um pouco os dedos em mais coisas de estruturas de dados e algoritmos que não coube no episódio passado. Então vamos lá.</p>
<p>(&hellip;)</p>
<p>Vamos começar com alguns erros pequenos. Tem um que se eu não prestar atenção cometo toda hora sem perceber, até eu acho irritante. Provavelmente porque por muitos anos programei em 32-bits, eu confundo quando converto bits em bytes. Mas é simples, só dividir por 8. Endereços de 32-bits são armazenados em 4 bytes. Mas endereços de 64-bits são 8 bytes. Só que minha memória muscular me ferra.</p>
<p>Aos 6 minutos e 6 segundos do video eu falo que se você alocar um inteiro de 64-bits mas não for usar valores maiores que 255, vai estar desperdiçando 7 bytes desses 8. E isso tá correto. Mas durante a edição a memória muscular ficou me falando “32 bits é quatro bytes”, o que tá errado, e achei que eu tinha gravado falando errado. Daí escrevi uma correção na tela de que iria desperdiçar 3 bytes. Ou seja, eu falei certo na gravação e corrigi errado na edição. A mesma coisa aconteceu aos 45 minutos e 20 segundos que eu falo que é 4 bytes, mas são 8.</p>
<p>Outra coisa que vira e mexe me confundo se falar muito rápido foi como aos 40 minutos e 54 segundos eu falo que 255 BYTES é um quarto de 1 megabyte. Tá errado, lógico, é um quarto de KILObyte. 255 vezes quatro são 1024 bytes, ou um kilobyte. Daí aos 6 minutos e 50 segundos eu falei errado o range de inteiros de 8-bits com sinal, o correto é de menos 128 a positivo cento e vinte e sete. Em hexa é mais difícil de errar porque vai de hexa FF até 7F.</p>
<p>Mais pro fim, depois dos 57 minutos quando faço a <code>struct</code> Person e começo a fazer createPerson escrevi errado <code>nome</code> em vez de <code>name</code> e ficou meio estranho. Foi um typo feinho mas como não chega a estragar o exemplo em si resolvi não regravar só por causa disso. Pior foi quando dei copy e paste do código de inicialização da struct pra dentro da função createPerson e esqueci os valores hardcoded em vez de usar os argumentos. O código ficou desse jeito aqui, mas o correto deveria ter editado pra ficar assim. Por isso a gente tem que tomar cuidado ao ficar dando copy e paste de código.</p>
<p>Além desses erros menores, o erro que me deixou mais irritado foi perto dos 39 minutos quando eu quis explicar sobre stack e passagem de parâmetros por valor. O exemplo original era com inteiros e nesse caso ela teria sido copiada 3 vezes passando da <code>main</code> pra <code>f1</code> e depois pra <code>f2</code>, mas eu substituí pela string “hello world” e na hora nem pensei muito porque eu tava com pressa, só segui o mesmo exemplo como se o string estivesse sendo duplicado na stack. Obviamente que não tá. E pra explicar, preciso complicar a parte que eu não queria. Quando falei de memória virtual pode parecer que ela é dividida só entre stack e heap, mas na verdade tem mais que isso.</p>
<p>Se eu quiser complicar e mostrar mais, vai ficar deste outro jeito. Seguindo pra baixo do heap a gente tem o segmento BSS que é o block starting symbol, ou que alguns chamam de better save space. Toda variável global estática que só foi declarada mas você não atribui nenhum valor fica nesse espaço, automaticamente inicializado como zero ou nulo. É diferente de quando você usa um <code>malloc</code> ou <code>calloc</code> que vai pegar algum trecho de memória disponível da heap.</p>
<p>Embaixo do BSS tem mais dois segmentos que interessa pra gente hoje, a ROData e a Text. Esse segmento Text, ao contrário do que você pode entender pelo nome, é o espaço de endereços do seu programa executável propriamente dito, é um segmento de código, das instruções em linguagem de máquina. Agora o segmento ROData é mais interessante pra gente.</p>
<p>Quando eu fiz <code>char hello[] = &quot;Hello World&quot;</code> eu tinha dito que isso seria alocado na stack, o string inteiro. Tá errado. Que burro, dá zero pra ele. E até podia ser assim mesmo numa outra linguagem, mas no caso de C, o hello vai ser uma referência pra um endereço que fica nesse espaço chamado <code>ROData</code>. Quando o compilador GCC passa pelo meu código fonte, ele vê dados como esse <code>hello</code> que é fixo e constante e grava dentro desse segmento no binário final. RO é de Read Only, então ROData é um segmento de dados somente de leitura.</p>
<p>Isso não altera a explicação de passagem por valor. Toda vez que eu passo a variável <code>hello</code> como argumento das funções <code>f1</code> e <code>f2</code>, cada uma vai empilhar uma nova cópia no stack, mas não do string inteiro, e sim dos 8 bytes de endereço que apontam pro array &ldquo;Hello World&rdquo; que é fixo, constante e tá dentro do segmento <code>ROData</code>.</p>
<p>Pense nesse segmento com uma versão estática do Heap, que é dinâmico. O ROData é pra valores que você tem hardcoded no seu código fonte. Como strings. Mas strings dinâmicas, por exemplo, de valores que vieram num JSON que você puxou via API, de linhas do banco de dados que você puxou da rede, tudo isso vai ser alocado no Heap e, mesma coisa, os ponteiros pra eles vão sendo empilhados na stack durante a execução do programa.</p>
<p>Quando eu falei do formato ELF binário, que é o linguição de bits num formato específico pro Linux executar, a primeira instrução começa no endereço hexa 1000. Mas no mesmo binário ELF existem outros segmentos que você pode inclusive ver usando o comando no shell <code>objdump -h</code> e o executável que acabou de compilar. Então, se passarmos esse objdump no nosso hello vão aparecer todos os segmentos. Você vê que o décimo primeiro é o segmento <code>init</code> no endereço hexa 1000 que eu falei antes e na décima quinta posição tem o endereço hexa 2000 que é o segmento ROData.</p>
<p>No video anterior, ignorem quando eu falo que o string inteiro é alocado no Stack. Ele tá no ROData e na stack as variáveis <code>hello</code> são referências com o endereço pra esse string. É algo assim. As referências ocupam espaço, pouco espaço se comparado a um arrayzão, mas ocupam. E quando são passadas por valor, de função pra função, vão sendo duplicadas na stack. Três vezes no nosso hello world, cada uma com 8 bytes. Mas não é pra você se preocupar com uso de espaço na stack, isso raramente é um problema. Como eu já tinha dito só quando seu programa é pesado em coisas como recursão.</p>
<p>Outra coisa que eu Não falei no episódio anterior quando expliquei ponteiros é que depois de chamar <code>malloc</code> pra alocar espaço no heap, quando você termina o que tinha pra fazer, a boa prática é chamar a função <code>free</code>, que avisa o gerenciador de memória que aquele segmento não tá mais em uso e ele pode usar pra outra coisa. Eu não fiz <code>free</code> pra explicação não ficar extensa mas também porque o programa executa e sai muito rápido. E quando um programa termina, o sistema operacional se encarrega de tornar toda memória que você tava usando disponível de novo pra outra coisa, independente se você deu free antes ou não.</p>
<p>Programinhas que executam rápido como um hello world não precisam se preocupar. O problema são programas que ficam abertos muito tempo, digamos um navegador, ou um servidor web. Tirando em exercícios como o hello world, em código de verdade, você DEVE se preocupar com a memória sendo usada porque elas vão ficar em uso por tempo indeterminado. Podem ser horas ou dias e se você ficar dando <code>malloc</code> e nunca der <code>free</code> a memória disponível no heap vai acabar ou chegar perto de acabar, e quando isso acontece mecanismos como o OOM do sistema operacional pode forçar a parada do seu aplicativo. OOM é o sistema que monitora a memória usada por aplicativos, que significa Out of Memory Killer.</p>
<p>Justamente porque gerenciamento de memória é complicado de explicar, eu já tinha feito dois vídeos que recomendo que vocês assistam. Uma das vantagens de linguagens de mais alto nível com Java, C#, Go e todas as interpretadas como Javascript, Ruby, Python, é que elas tem algum garbage collector, cuja responsabilidade é gerenciar o heap no lugar do programador. Porém, a comodidade vem com um custo: todo garbage collector sempre vai usar mais memória do que se você gerenciar manualmente e causar picos de pausa pra limpeza durante o processamento, o que pode ser um problema pra você. Por outro lado tem menos riscos de vazar memória por causa de código mal feito.</p>
<p>As duas linguagens modernas que tem um bom balanço entre comodidade e não desperdiçar muita memória são o Swift e o Rust. Recomendo estudar o ARC do Swift que é uma ajuda do compilador pra gerenciar memória via contador de referências e o sistema de Borrow do Rust que é o mais diferente de todos.</p>
<p>Voltando aos meus erros, outra coisa que eu falei errado no video anterior foi quando disse que o Stack, ou Pilha, tem dois tipos, LIFO e FIFO e logo na sequência falei que tem Queues ou filas. Eu bobeei aqui, uma cadeia onde a gente vai empilhando elementos no topo e desempilhando do topo é last in first out, LIFO que é uma Pilha. Uma cadeia onde a gente vai empilhando elementos no fim mas vai tirando do começo, ou seja, vai tirando por ordem de chegada, é first in first out, FIFO também conhecido como Fila. Então toda pilha é LIFO e toda fila é FIFO.</p>
<p>Pilhas são usadas na execução de quase todo programa. Filas você vê todo dia no supermercado ou banco, ou se é desenvolvedor web. Todo servidor web tem uma fila, as requisições http vão chegando e sendo enfileiradas e vão sendo servidas na ordem de chegada. Todo job em background usa uma fila. Tudo que você precisa segurar porque não dá pra processar tudo de uma vez usa uma fila. Em sistemas distribuídos é particularmente importante pra coordenar e controlar a carga. Não adianta sua linguagem ser rápida em conseguir iniciar um monte de jobs assíncronos e sobrecarregar a máquina. Você precisa controlar quanto processamento paralelo quer ter num determinado momento, e filas é a estrutura básica por baixo desses tipos de mecanismos de controle.</p>
<p>E antes que alguém comente, eu disse que “quase” toda execução de programas coloca frames no stack mas existem linguagens chamadas stackless, que significa “sem stack”, embora isso não seja totalmente verdade. Inclusive existe uma versão de Python stackless. Não é que elas não usam nada do stack, mas elas criam um stack próprio no heap também. É um assunto longo por si só então só quis mencionar pra quem tiver curiosidade, mas pode ser interessante em casos de muita recursão ou concorrência onde você separa a stack de cada thread pra evitar coisas como condições de corrida.</p>
<p>Continuando, ficou outra dúvida nos comentários sobre arrays, porque eu disse que array é uma sequência contínua de elementos de mesmo tamanho, ou mesmo tipo. Tipo, em C, meio que define o tamanho do elemento. Se for um uint8_t é um inteiro sem sinal de 8-bits, portanto o tamanho é de 1 byte. Uma struct como a Person que eu mostrei, tem 3 campos, uma string de 10 chars, depois 2 ints de 8-bits, portanto 12 bytes no mínimo. O tamanho e formato do binário é definido pelo compilador e não vai ser necessariamente na ordem que eu mostro no exemplo.</p>
<p>Mas num Javascript eu posso fazer arrays conterem strings, números, outros arrays e o que eu quiser. Então ele é um array ou não é um array? Sendo honesto nunca parei pra ver o código em C++ que implementa Arrays de Javascript mas se ele for parecido com a forma que outras linguagens como Java ou C# da vida fazem ele provavelmente é um array de ponteiros mesmo.</p>
<p>Se o espaço do Array acaba e você tenta inserir um novo elemento, ele vai alocar um novo array um pouco maior e copiar todos os elementos da antiga pra nova, e no final liberar o espaço do array original. E todos os elementos dentro vão ser ponteiros, endereços pra objetos que pode ser objeto de string, objeto de número, objeto de array. Esses objetos estão alocados em algum lugar no heap, mas nesse array não vai ter o objeto em si, só o endereço que aponta pra esses objetos. E todos os endereços vão ocupar o mesmo tamanho de 8-bytes, portanto são todos elementos de mesmo tamanho, exatamente o requerimento pra um array.</p>
<p>Se uma linguagem suporta coisas como Generics, daí você consegue limitar um array de objetos pra ter elementos só de um determinado tipo, como Strings, e nunca aceitar um objeto de número por exemplo. Por baixo dos panos, no binário, não faz diferença porque tudo vai ser um ponteiro. Mas importa pra linguagem. Tipos em C eu acho mais importante pra determinar o layout binário das coisas. Tipos em linguagens de alto nível como Java é pra determinar a semântica do seu código.</p>
<p>Se você nunca estudou estruturas de dados sempre vai coçar a cabeça de porque um Java da vida tem classes como ArrayList, LinkedList, Vector, ArrayDequeue, HashSet, e tantas outras que herdam das interfaces de Iterable, Collection, Set e List. Sem conhecimento prévio, à primeira vista, tudo parece só um array.  É impossível eu explicar tudo, mas quero tentar explicar dois tipos diferentes de coleções hoje, que são dois dos mais usados pra vocês entenderem porque tem diferença.</p>
<p>Recapitulando, a grande vantagem de um array é rapidamente encontrar os valores de qualquer posição, com a premissa que você sabe que posição quer acessar. Só pegar o endereço do primeiro elemento e multiplicar pela posição e pelo tamanho do elemento, e por isso é importante todo elemento ser do mesmo tamanho. A desvantagem é se esse array precisa crescer o tempo todo e você não sabe o tamanho final. Daí toda vez que chega no final, precisa criar um outro array maior pra poder continuar, copiar todos os elementos do anterior e liberar essa memória, e ficar fazendo isso até conseguir preencher com todos os dados que precisa.</p>
<p>Vamos fazer de conta que na sua linguagem favorita ele instancia um array de 10 elementos por padrão. Daí você conecta num banco de dados, faz uma pesquisa e começa a vir linhas de dados. Você vai colocando na posição 0 do array, a próxima linha na posição 1, depois na posição 2 e vai indo. Aí preenche as 10 posições mas ainda tem mais linhas vindo do banco de dados. Por baixo dos panos, vai alocar outro array, agora com 20 posições, copiar as 10 da anterior, liberar o espaço dessas 10 antigas e agora você continua preenchendo, posição 10, posição 11 e vai indo.</p>
<p>Digamos que no final vieram 100 linhas da tabela do banco de dados. Pra comportar isso na memória, seu array precisou ser expandido 10 vezes. Ou seja, teve 10 operações de alocar um novo array e copiar o anterior nele. E na última etapa, quando você tinha um array de 90 elementos e acabou espaço, ele precisou alocar um de 100 elementos. Ou seja, num determinado momento você ocupou o dobro de memória, além de ter que fazer operações de alocação e cópia de todos os elementos até 9 vezes. É um desperdício de processamento e de memória.</p>
<p>Imagina se fosse uma tabela com cem mil linhas. Ou no outro extremo, imagine se você precisasse de 100 arrays que só vão ter 2 elementos cada. Nas no nosso faz de conta, cada array começa com 10 posições pré-alocadas. Agora você tem 800 posições que vão ficar desocupadas, mas ainda assim ocupando espaço na memória. Então a linguagem não pode nem pré-alocar um array grande demais pra pra não ter que processar muitas operações de alocar novo array e copiar tudo e nem pode pré-alocar muito pequeno pra economizar memória. É um dilema, por isso sempre que possível você deve iniciar arrays dizendo pra ele quanto de espaço pretende usar.</p>
<p>Se você está no caso de listas que vão expandir e você não sabe quanto, pode escolher não usar arrays. Vamos começar usando o struct que aprendemos e criar uma chamada <code>Node</code> que traduz pra um nó, em português. Dentro vamos criar dois elementos, um valor inteiro pra guardar qualquer número, e eu vou usar número só porque Strings em C puro é mais chatinho e ia complicar o exemplo, mas eu recomendo de lição de casa vocês tentarem fazer o valor do nó ser um String. O próximo elemento é mais importante, vou chamar ele de <code>previous</code> pra apontar pro nó que foi criado antes desse. Esse é o truque da lista ligada, é uma lista que liga elementos um atrás do outro ou um na frente do outro, como você preferir.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;stdbool.h&gt;

struct Node {
	int value;
	struct Node *previous;
};

struct Node* createNode(int value, struct Node *previous) {
	struct Node *node = (struct Node*) malloc(sizeof(struct Node));
	node-&gt;value = value;
	if (previous) {
		node-&gt;previous = previous;
	}
	return node;
}

void main() {
	struct Node *first = createNode(2020, NULL);
	struct Node *second = createNode(2021, first);
	struct Node *third = createNode(2022, second);


	struct Node *iterator = third;
	while(iterator) {
		printf(&#34;%d, &#34;, iterator-&gt;value);
		iterator = iterator-&gt;previous;
	}
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Pra facilitar, vamos criar uma função que aloca e inicializa um nó, semelhante ao que fizemos no video anterior com a struct Person. Como argumentos ele vai receber um valor inteiro qualquer e o nó que foi criado antes pra ele apontar pra ele. Dentro vamos usar <code>malloc</code> pra alocar essa nova struct, depois preenchemos o campo <code>value</code> com o que veio como primeiro argumento e checamos se o segundo argumento veio nulo. No caso, o primeiro Node criado não aponta pra ninguém, então deixa ele nulo, senão preenche o campo <code>previous</code> pra apontar pro nó que passamos.</p>
<p>Agora no <code>main</code> vamos criar 3 nodes bestas, um com valor de 2020 e mandando NULL que é nulo, porque é o primeiro nó. O segundo nó vai ter 2021 e passamos a referência pro <code>first</code> que criamos antes. Mesma coisa pro terceiro nó que passamos o valor 2022 e a referência do segundo nó. Agora que a coisa fica interessante.</p>
<p>Vou criar um quarto ponteiro, chamado <code>iterator</code> que vai apontar pro mesmo endereço da variável <code>third</code>, a última. Assim começamos um loop que vai parar quando a variável <code>iterator</code> for nulo. Nulo em C e várias linguagens interpreta pra <code>false</code> e quando der isso o loop <code>while</code> vai sair. A cada rodada do loop vamos imprimir o valor inteiro do nó na tela separado por vírgula usando nossa conhecida função <code>printf</code>. Pra quem não sabia ele formata os valores que passamos pra ele. Então &ldquo;%d&rdquo; é de decimal, daí ele formata números. No vídeo anterior eu usei &ldquo;%x&rdquo; que formata números como hexadecimais e &ldquo;%s&rdquo; é pra interpretar como um string mesmo.</p>
<p>E finalmente, depois de usar o valor do node, fazemos a variável iterator apontar pro endereço que tá no campo <code>previous</code>. Como o iterator começou sendo a variável third, e como o previous dela aponta pro endereço que tava na variável second, é isso que ele vai se tornar. Daí o loop volta pro começo, checa que a variável iterator ainda não é nulo e imprime o valor que tá na struct apontada por second. Finalmente fazemos o iterator ser o endereço que tá no campo previous desse second, que vai ser o first, e repetimos o loop.</p>
<p>Como o iterator agora aponta pra mesma struct que a variável first, ela continua não sendo nula. Imprime o valor de first, e o iterator vai ser o que estiver no campo previous dela. Como ela foi a primeira a ser criada e passamos NULL no segundo argumento quando criamos, ela não aponta pra mais ninguém. Agora o iterator vai ser NULL e isso vai parar o loop do <code>while</code>, e também vai terminar a função main porque não tem mais nada pra fazer, então o programa acaba e sai.</p>
<p>Um Node ligado a outro Node forma uma lista de Nodes ligados. Por isso falamos em Lista ligada que é outra estrutura de dados importante na computação. Ela tem várias vantagens em cima de um array. Pra começar, é fácil adicionar novos nodes. Só alocar uma struct no Heap e fazer o campo de ponteiro dela apontar pro último nó que foi criado. Assim não precisamos pré-alocar espaço que não vamos precisar. Mais do que isso. Já pensaram como faz pra inserir um elemento novo no meio de um array?</p>
<p>Como o array é sequencial e fixo, você é obrigado a fazer alguma coisa como copiar todos os elementos do meio pro final pra outro lugar, inserir o valor novo no meio, e copiar todos os elementos que guardamos de volta. Vai desperdiçar espaço e processamento. No caso de uma lista ligada basta alocar uma nova struct e mudar os ponteiros dos nodes ao redor da posição onde queremos inserir. Fazemos o elemento seguinte apontar pra ele e a nova struct apontar pro elemento anterior e pronto, inserimos no meio. O custo de inserir um novo elemento no meio é praticamente igual a inserir no fim, e independe do tamanho da lista.</p>
<p>Mas então, porque não usamos listas ligadas pra tudo? Porque ela tem uma grande desvantagem. Repetindo, num array, pra eu pegar o valor do centésimo elemento, basta pegar o endereço do primeiro e multiplicar o tamanho de cada elemento vezes 100 e imediatamente chegamos. Mas numa lista ligada, preciso pegar a primeira struct, e ir seguindo os ponteiros pro próximo elemento 100 vezes. Então quanto maior for a lista ligada, mais lento vai ser achar elementos nela, porque cada struct fica em algum lugar aleatório no heap e não tem como achar só com aritmética de ponteiro.</p>
<p>Arrays e Listas Ligadas, à primeira vista, especialmente se tiver poucos elementos, funcionam praticamente igual. Você nunca vai notar a diferença de operações ou performance porque mexer em listas de 10 elementos, cem elementos, mesmo mil elementos, é muito rápido na sua máquina. Mas quando você conecta num banco de dados, ou precisa processar centenas de arquivos grandes, e precisa alocar listas pra manipular todos esses dados, usar arrays ou listas vai fazer uma grande diferença de performance. E sem saber como elas funcionam, você jamais vai entender o que tá dando errado.</p>
<p>E tem outras estruturas importantes. A próxima que eu quero explicar pra você é um Map, ou HashMap ou Hashtable ou dicionário. Tem vários nomes por diversas particularidades de implementação. Na maioria das linguagens você provavelmente chama só de Hash. É quando você cria uma lista, mas em vez de navegar nela direto por posição, navega usando uma chave.</p>
<p>Em Javascript é o que você chamaria de objeto JSON. Por exemplo, podemos criar uma variável person com os campos name, age e city, preencher com Fabio, 43, Sao Paulo e podemos acessar os valores com a sintaxe de colchetes que nem um array mas passando o nome do campo, como &ldquo;age&rdquo; e ele devolve 43. Se você não sabia disso, apesar da sintaxe parecer um array, isso não é um array e por baixo funciona bem diferente.</p>
<p>Existem diversas formas de implementar Hashtables. Quero mostrar uma forma rudimentar em C. Antes vamos ver como se usa. Na função <code>main</code> eu quero alocar primeiro uma struct chamada Hash que vamos criar depois. Daí vamos criar uma função chamada <code>insertNode</code> que recebe o ponteiro pra esse hash e vamos inserir alguns dados. Primeiro um valor &ldquo;world&rdquo; com uma chave &ldquo;hello&rdquo;. Se fosse javascript seria hash abre colchetes, daí &ldquo;hello&rdquo; igual &ldquo;world&rdquo;. Apesar da sintaxe não ser bonitinha com colchetes, pense no funcionamento como sendo a mesma coisa do equivalente em Javascript.</p>
<p>Vamos fazer mais um <code>insertNode</code> pro mesmo hash, mas com chave &ldquo;Fabio&rdquo; e valor &ldquo;Akita&rdquo;. Agora vamos imprimir na tela esses valores procurando pela chave. Se fosse javascript seria o equivalente a um console ponto log entre parênteses hash colchetes com a chave &ldquo;hello&rdquo;. Mas a gente vai fazer uma função chamada <code>search</code> que recebe como parâmetro o ponteiro do mesmo hash e a chave &ldquo;hello&rdquo;. Daí passamos o valor que ele vai retornar pro <code>printf</code> imprimir na tela.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;stdbool.h&gt;

#define HASH_SIZE 100

struct Node {
	char *key;
	char *value;
	struct Node* next;
};

struct Hash {
	struct Node *list[HASH_SIZE];
};

unsigned int hashCode(char *key) {
	unsigned long hash = 5381;
	unsigned int c;
	while(c = *key&#43;&#43;) {
		hash = ((hash &lt;&lt; 5) &#43; hash) &#43; c; // hash * 33 &#43; c
	}
	return hash % HASH_SIZE;
};

struct Node* createNode(char *key, char *value) {
	struct Node *node = (struct Node*) malloc(sizeof(struct Node));
	node-&gt;key = key;
	node-&gt;value = value;
	return node;
};

void insertNode(struct Hash *hash, char *key, char *value) {
	unsigned int index = hashCode(key);
	struct Node *node = hash-&gt;list[index];
	if(node == NULL) {
		hash-&gt;list[index] = createNode(key, value);
	} else {
		while(node) {
			if (node-&gt;next == NULL) {
				node-&gt;next = createNode(key, value);
				break;
			}
			node = node-&gt;next;
		}
	}
};

char* search(struct Hash *hash, char *key) {
	unsigned int index = hashCode(key);
	struct Node *node = hash-&gt;list[index];
	while(node) {
		if (strcmp(node-&gt;key, key) == 0) {
			return node-&gt;value;
		}
		node = node-&gt;next;
	}
	return &#34;&#34;;
}

void main() {
	struct Hash *hash = (struct Hash*) malloc(sizeof(struct Hash));
	insertNode(hash, &#34;hello&#34;, &#34;world&#34;);
	insertNode(hash, &#34;Fabio&#34;, &#34;Akita&#34;);

	printf(&#34;%s\n&#34;, search(hash, &#34;hello&#34;));
	printf(&#34;%s\n&#34;, search(hash, &#34;Fabio&#34;));
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Agora sabemos que precisamos implementar uma struct chamada Hash e pelo menos duas funções, a <code>insertNode</code> e a <code>search</code> que é procurar em inglês. Vamos começar pela struct Hash que vai conter uma variável chamada <code>list</code> que é um array de ponteiro pra struct chamado <code>Node</code>. O hashtable vai ser implementado com um array de tamanho fixo. Na prática nem precisava ser uma struct Hash, eu podia só ter criado direto um array, mas só pra ficar mais fancy.</p>
<p>Só pra ficar menos feio vamos definir essa constante 100 com o nome de HASH_SIZE lá em cima usando o pré-processamento #define, que eu expliquei no episódio anterior. É uma forma de não largar números mágicos por aí. Quando passarmos pelo compilador, imagine que vamos dar copy e paste do número 100 no lugar desse nome HASH_SIZE.</p>
<p>Agora temos que implementar a struct principal, que é a <code>Node</code> que vai conter os campos principais de chave e valor que são as strings <code>key</code> e <code>value</code>. Depois, como na lista ligada, um ponteiro pra <code>next</code> que vai ser o próximo Node na cadeia. De propósito antes eu mostrei uma lista ligada que vai apontando pro Node anterior. Aqui vai ser uma lista ligada na direção oposta, apontando pro próximo Node mais novo, só pra mostrar que tanto faz a direção. Dependendo do exemplo que você achar por aí vai ter jeitos diferentes de implementar.</p>
<p>Agora vamos implementar a função mais importante de um hashtable que é a função que converte a chave num identificador numérico. No nosso caso a chave vai ser só strings. Esse número identificador é o que chamamos de hash e eu expliquei mais sobre hashing nos episódios de criptografia, então dêem uma olhada depois. Grosseiramente é como se fosse um gerador de números aleatórios ou random, onde o seed ou semente é o valor da tal chave.</p>
<p>Gostaríamos que essa função de hash devolvesse números o mais aleatório possíveis, ou seja, cuja distribuição estatística seja próxima de uma curva normal ou gaussiana. Se não entendeu isso, coloque na lista estudar estatística básica. A razão disso é simples. A struct Hash que criamos vai conter um ponteiro pra um array de tamanho fixo. Num hashtable podemos inserir mais elementos do que o tamanho do array. A mágica pra isso começa em transformar a chave que é um string num número entre 0 e 99. Uma função de hash pega, por exemplo, a string &ldquo;hello&rdquo; e converte num número, por exemplo esse numerozão aqui do lado.</p>
<p>Pra isso vamos fazer uma função chamada <code>hashCode</code> que recebe essa chave. Se você é de Java já viu esse nome. Dentro começamos declarando um número mágico, no caso cinco mil trezentos e oitenta e um. Poderia ser qualquer número. Depois declaramos uma variável chamada <code>c</code> que a cada rodada do loop vai pegando uma letra da chave. Esse loop vai repetindo até acabar as letras da chave e o <code>c</code> ficar nulo e parar o loop.</p>
<p>O cálculo dentro do loop não é importante pra hoje, tem várias outras formas. Em particular neste exemplo começa com o valor inicial da variável hash que é o cinco mil e tantos acima. Daí multiplicamos por 33 que é o mesmo que fazer bitwise shift pra esquerda de 5 bits, somamos ao hash original junto com o endereço da primeira letra. E vamos fazendo isso letra a letra até o loop parar. No final vamos ter um numerozão que identifica essa chave. Esse número pode ser bem maior do que o tamanho do array e a gente gostaria que fossem posições válidas dentro do nosso array.</p>
<p>Sempre que você tem um range ou sequência limitada, no nosso caso que vai de 0 a 99, mas temos um número maior que isso, pra caber, usamos a operação de módulo. E aqui é mais um exemplo de porque matemática importa. Vamos precisar de módulo. Na prática um módulo faz um wrap. Qualquer número módulo 100, vai devolver um número menor que 100. Daí podemos usar isso como um índice ou hash. Toda chave sempre vai chegar no mesmo hash, mas a partir do hash não temos como voltar ao valor original da chave.</p>
<p>O cálculo do exemplo chamado de djb2. Não é o único e nem é o melhor, mas é fácil pra explicar pra hoje em uma linha só. Lembram quando eu disse que vídeos como esse e qualquer outro tutorial e curso priorizam a facilidade da explicação? Entenda que existem outros algoritmos mais sofisticados do que multiplicar por 33 e ir acumulando. Especialmente algoritmos estatisticamente melhor distribuídos. Mas por agora, o importante é entender que esta é uma das maneiras de converter um string num número e com o módulo, garantidamente devolver uma posição dentro do Array.</p>
<p>Antes de continuar, vamos criar uma função que instancia e popula uma struct Node. Podemos chamar de <code>createNode</code> que recebe como argumentos os strings de chave e valor, <code>key</code> e <code>value</code>. Dentro fazemos <code>malloc</code> pra reservar espaço pra essa struct. Em seguida populamos os campos <code>key</code> e <code>value</code> da struct com os valores que recebemos de argumento. Bem simples. Vamos manter o campo <code>next</code> da struct como nulo.</p>
<p>Como aprendemos no episódio anterior, ao usar a notação de setinha que é um traço e sinal de maior, ele acessa o valor do ponteiro de item que é a struct de Item que tá em algum lugar no Heap. Essa sintaxe de setinha é equivalente a fazer asterisco variável entre parênteses e usar a notação de ponto. Mas setinha fica mais bonito.</p>
<p>Finalmente chegamos ao filé mignon da explicação implementando a função <code>insertNode</code> que vai de fato armazenar o par de chave e valor dentro do nosso array fixo. Começamos com função <code>hashCode</code>, passando a <code>key</code> que veio como argumento e achando a posição numérica dentro do array, que vai ser essa variável <code>index</code>. Continuamos declarando uma variável node que vai apontar pro primeiro Node na posição index do arrayzão.</p>
<p>A lógica vai assim: se no array da estrutura de hash, na posição que chamamos de <code>index</code>, não existir nada, se for nulo, então criamos um novo Node com a função <code>createNode</code>, passando key e valor e armazenamos o endereço desse Node na posição <code>index</code> do Array chamado <code>list</code> dentro da estrutura que chamamos de hash.</p>
<p>Caso contrário, se na posição <code>index</code> já existir um Node, temos duas opções. Se no campo <code>next</code> desse Node não existir nada, for nulo, chamamos <code>createNode</code> pra criar um Node e associamos seu endereço no campo <code>next</code>. Agora temos uma lista ligada de dois elementos. Podemos quebrar o loop e sair fora. Se a lista tiver mais de um elemento já, esse <code>next</code> já vai estar apontando pra outro Node. Então fazemos a variável <code>node</code> apontar pra esse <code>next</code> e repetimos o loop, até achar o último Node cujo <code>next</code> é nulo e nele associamos nosso novo Node.</p>
<p>Essa nossa variável <code>list</code> na struct Hash não é um array simples como mostramos até agora. É um array onde cada posição vai apontar pra uma lista ligada, ou seja, é tipo uma matriz. O conceito é assim: cada lista ligada pode ter quantos elementos quisermos. Daí nesse array fixo vamos conseguir colocar mais elementos do que o tamanho total de posições só do array. Estamos combinando o melhor do array com o melhor da lista ligada numa estrutura só.</p>
<p>Outro conceito importante é que o número que o <code>hashCode</code> devolve identifica uma ou mais chaves. Já disse que ela não é única pra cada chave. Especialmente porque fazemos o módulo do hash pra ter a posição do array. Se múltiplas chaves podem chegar no mesmo número significa que mais cedo ou mais tarde vamos ter uma colisão, ou conflito. Esse é o termo mesmo. Quando acontecer uma colisão, dois Nodes vão competir pela mesma posição no array.</p>
<p>Vamos dar um exemplo, faz de conta que queremos inserir um valor &ldquo;bla&rdquo; com uma chave &ldquo;abc&rdquo;. E faz de conta que queremos inserir outro valor &ldquo;foo&rdquo; com uma chave sendo &ldquo;xyz&rdquo; e agora o principal, faz de conta que o cálculo da nossa função <code>hashCode</code> devolva o número 42 pra ambos os strings. Vamos simular um caso de colisão.</p>
<p>Da primeira vez, vamos pra posição 42 do array <code>list</code> e apontamos pra uma struct Node com a chave sendo &lsquo;abc&rsquo; e valor sendo &lsquo;bla&rsquo;. Na segunda tentativa o hash da chave &lsquo;xyz&rsquo; devolve o mesmo índice 42, mas na posição 42 do array já tem o Node com chave &ldquo;abc&rdquo;, então criamos um novo Node e fazemos o primeiro apontar pra ele. Agora temos os dois armazenados na posição 42 do array, entenderam?</p>
<p>Pra ficar claro, digamos que queremos incluir um terceiro par de valores cuja chave é “foo” e o valor é “bar” e de novo, faz de conta que o hashCode pra essa chave “foo” seja 42 também. Agora já existe uma lista ligada nessa posição do array. Começamos do primeiro Node com chave “abc”. O <code>next</code> dela aponta pra outro Node, com a chave “xyz” e agora o <code>next</code> é nulo, portanto é o fim da lista. Basta fazer esse <code>next</code> apontar pro novo Node com chave “foo” e pronto, adicionamos mais um elemento no hashtable. É isso que acontece num caso de várias chaves colidirem e competirem pela mesma posição no array.</p>
<p>De volta ao exemplo, na função <code>main</code> usamos a função <code>insertNode</code> duas vezes, na primeira pra inserir o par com chave <code>hello</code> e valor <code>world</code> e na segunda vez pra inserir o par com chave <code>Fabio</code> e valor <code>Akita</code>. A chave <code>hello</code> passando pela nossa função hashCode devolve o número 41 e passando a chave <code>Fabio</code> devolve o índice 90. Ou seja, a posição 41 do array <code>list</code> vai apontar pra uma instância da estrutura Node com o valor &ldquo;world&rdquo; e na posição 90 do array <code>list</code> vai existir outra instância de Node com o valor <code>Akita</code>.</p>
<p>Só pro nosso exemplo ficar mais interessante faz de conta que antes de inserir a chave <code>hello</code> já tinha um Node com chave <code>ano</code> e valor <code>2021</code> e faz de conta que se passarmos a string <code>ano</code> pra função hashCode vai dar o número 41. Daí a gente tenta inserir a chave <code>hello</code> e valor <code>world</code> e passando <code>hello</code> em <code>hashCode</code> sabemos que volta 41 também. Criamos um novo Node com a chave “hello” e colocamos seu endereço no ponteiro <code>next</code> do Node chave <code>ano</code>.</p>
<p>Esse é o mecanismo. O pior caso é quando a função de hashCode dá colisões demais e a maioria dos elementos que tentamos inserir acaba na mesma posição do array, e aí fica desbalanceado, por isso escolher a melhor fórmula vai fazer diferença e por isso eu falei em uma função que tivesse uma distribuição gaussiana, onde na média, cada posição do array vai ter mais ou menos o mesmo número de elementos.</p>
<p>Finalmente, uma vez inserido, e se a gente quiser achar o valor que corresponde à chave <code>hello</code>? Vamos criar a função <code>search</code> que devolve um ponteiro pra uma String, que vai ser esse valor. E como parâmetro vamos receber a referência pra struct Hash que contém o array <code>list</code> e como segundo parâmetro um ponteiro pra string que contém a chave que estamos procurando, no caso &ldquo;hello&rdquo;.</p>
<p>De novo, começamos calculando o tal hash da chave &ldquo;hello&rdquo; que já sabemos que vai ser 41. Daí pegamos o primeiro Node da lista ligada que tá na posição 41 do nosso array <code>list</code>. Agora começamos um loop. Enquanto a variável <code>node</code> não for nulo, entra no corpo do Loop. Dentro comparamos o valor da chave nesse Node com a chave que recebemos no parâmetro da função usando essa função do C chamada <code>strcmp</code> ou string compare, que vai devolver <code>zero</code> se as duas strings que passamos tiverem o mesmo valor.</p>
<p>No nosso exemplo vai dar false porque o primeiro Node contém um Item cuja chave é <code>ano</code>. Sendo assim, pulamos esse <code>if</code> e fazemos a variável <code>node</code> apontar pro <code>next</code> do atual. Repete o loop, a variável <code>node</code> ainda não é vazio. No <code>if</code> comparamos de novo o string do campo <code>key</code> com a variável <code>key</code> do parâmetro. E como esse segundo Node contém a chave <code>hello</code>, o <code>strcmp</code> vai dar verdadeiro e podemos retornar o campo <code>value</code> do Node, que vai ser <code>world</code>.</p>
<p>Pronto, em conceito é mais ou menos assim que funciona um hashtable pra inserir e procurar um Node. E é assim que conseguimos inserir centenas ou milhares de itens num array que é limitado em cem posições. Toda vez que tiver uma colisão de chave, anexamos o Node no final da lista ligada nessa posição do array. E quando for procurar a chave, calculamos o hash que dá a posição no array e vamos seguindo a lista ligada que está lá até encontrar o Node com a chave que procuramos.</p>
<p>A essa altura você deve estar pensando, &ldquo;por que todo esse trabalho&rdquo;? A vantagem é que se nosso array estivesse todo cheio, e quisermos achar onde tá o Node com a chave “hello”, rapidamente sabemos que tá na posição 41 do array, porque o cálculo do hash independe de quantos elementos existem. Mesmo se a hashtable tiver 200 Nodes já, chegamos na posição 41 com um simples cálculo. Daí só precisamos fazer mais 2 comparações de chaves na lista ligada pra ir de next em next até chegar no Node com chave “hello” e vamos achar o valor “world”.</p>
<p>Se usássemos só um array pra inserir 200 elementos, mas o array começa com 10 elementos pré-alocados, ia precisar expandir e copiar todos os elementos do array 20 vezes. Mas pra achar um valor numa posição específica é rápido. Por outro lado, se usássemos uma lista ligada pra inserir os 200 elementos, a inserção seria super rápida e econômica. Mas pra procurar teríamos, no pior caso, que vascular 200 elementos. Mas juntando o array com a lista ligada, diminuímos consideravelmente tanto o tempo de inserção quanto o tempo de procura.</p>
<p>Mas da forma como implementamos, não só agora eu posso usar uma string como chave em vez de um número, como estamos segmentando, ou seja, dividindo a lista ligada dentro de um array fixo. E isso nos dá o meio do caminho do melhor dos dois mundos entre um array e uma lista ligada. Não vamos desperdiçar memória e processamento tendo que duplicar o array toda vez que chega no fim. E ao mesmo tempo podemos procurar itens razoavelmente rápido porque em vez de vasculhar uma lista ligada inteira, vamos vasculhar só um trecho dela.</p>
<p>Então, toda vez que você fizer um hash em Javascript ou outras linguagens que suportam essa sintaxe de colchetes, por baixo dos panos é algo parecido com isso que tá acontecendo. Nessa nossa implementação rudimentar, esse <code>search</code> passando o <code>hash</code> e argumento <code>hello</code> seria o equivalente no Javascript a escrever <code>hash</code> colchetes e <code>hello</code> dentro.</p>
<p>Falando em Javascript, muitos que estão iniciando os estudos agora talvez já tenham visto exercícios implementando coisas como listas ligadas ou hashtables em Python, Javascript ou outra linguagem de alto nível. E de fato, eu poderia implementar exatamente esse hashtable que fiz em C só que em vez de usar structs eu poderia usar classes. Daí você poderia ter a péssima idéia de usar esse hashtable que escreveu em vez de usar a que já vem por padrão na linguagem.</p>
<p>Isso é uma má idéia porque nenhuma versão de hashtable que você escrever em Python puro vai ser mais rápido do que a que já vem nela. A explicação é simples. Vamos ver como o Python implementa o hashtable, que ele chama de <code>dict</code>. O Python mais usado é o CPython, e esse nome é porque o Python é escrito em C. E se a gente for no GitHub e vasculhar o código fonte do CPython vamos achar o arquivo <code>dictobject.c</code> que, chan chan chan chan, é escrito em C. Esse em particular tem mais de 5 mil linhas de C. Por isso eu repito, o exemplo que eu dei antes é uma simplificação rudimentar pra ser didático de explicar. Pra ver o de verdade, você precisa ver esse do Python.</p>
<p>A mesma coisa vale pra todas as estruturas de dados a algoritmos básicos de ordenação, procura e coisas mais avançadas como criptografia que o Python te dá. Arrays, listas ligadas, sets que são conjuntos e muito mais. E a mesma coisa acontece em todas as outras linguagens de alto nível como Ruby, como Javascript. Eu disse no episódio anterior que elas são ótimas linguagens pra colar estruturas feitas em C ou C++ com uma sintaxe de mais alto nível e mais fácil de usar. É isso que eu quero dizer.</p>
<p>Se você estudou ou for estudar a matéria de estruturas de dados e algoritmos, vai aprender a operar essas estruturas. Até agora eu só mostrei a coisa mais trivial que é inicializar e popular as estruturas. Mas e se quisermos procurar elementos num array gigante? Ou se quisermos ordenar em ordem alfabética? Daí você começa a aprender sobre algoritmos. Sem ter estudado nada, pense, como você ordenaria um array?</p>
<p>Ordenação é um dos tópicos da matéria e normalmente o primeiro algoritmo que você aprende é o famigerado bubblesort. Pra acelerar o código é este aqui do lado. Mas só ver o código talvez não ajude muito. Se você procurar no Google por sorting algorithms animation vai achar vários sites que mostram uma versão animada de como cada uma funciona. Vamos ver um bubble sort em ação. Conseguem ver como ele vai comparando pares de números e fazendo os números maiores irem pra direita? Compara um par, se o da esquerda for maior que o da direita, faz um swap e troca eles de lugar e vai fazendo isso até o fim da lista, e repete, até tudo estar ordenado.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;

void main() {
	int total = 15;
	int array[] = { 40, 55, 11, 32, 67, 5, 74, 89, 38, 66, 27, 36, 79, 99, 2 };

	int i, j, swap;

	for(i = 0; i &lt; total - 1; i&#43;&#43;) {
		for(j = 0; j &lt; total - i - 1; j&#43;&#43;) {
			if(array[j] &gt; array[j&#43;1]) {
				swap = array[j];
				array[j] = array[j&#43;1];
				array[j&#43;1] = swap;
			}
		}
	}
	printf(&#34;Ordenado: &#34;);
	for(i = 0; i &lt; total - 1; i&#43;&#43;) {
		printf(&#34;%d, &#34;, array[i]);
	}
	printf(&#34;%d\n&#34;, array[i]);
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>No exemplo em C é assim. Começamos declarando uma variável total como sendo um número hardcoded 15 e em seguida criamos um array de 15 números fora de ordem qualquer. Daí precisamos de 3 variáveis inteiros pra ajudar, um chamado <code>i</code>, outro <code>j</code> e um chamado <code>swap</code>. Começamos um loop fazendo o <code>i</code> ir de 0 até o total menos um que é 14. Lembrando que arrays em C começam de 0 e vai até o total menos 1. Então um array de 15 elementos vai ter posições de 0 até 14.</p>
<p>Dentro tem outro loop, fazendo <code>j</code> ir de 0 também até o total menos <code>i</code>. Isso porque dentro comparamos o elemento na posição <code>j</code> com o seguinte que é <code>j</code> mais um, então não pode ir até o último senão vamos tentar pegar um décimo sexto elemento que não existe e vai dar pau. Daí comparamos entre o elemento na posição <code>j</code> com o de <code>j</code> mais um e se o de <code>j</code> for maior, fazemos o swap. Swap nesse caso é guardar o elemento de j mais um, copiar o elemento de j pra j mais um, e na posição j gravar o que tava na variável swap. Em outras linguagens tem como fazer swaps assim com uma operação só, mas por baixo dos panos sempre vai ter uma variável intermediária.</p>
<p>Se achou complicado depois tente você mesmo executar esse código em C com diferentes arrays, usando <code>printf</code> em cada etapa pra ver o que tem nas variáveis <code>i</code> ou <code>j</code> e o que tem no array nesses posições pra entender o que tá acontecendo. Mas o importante é entender o seguinte. Temos um loop dentro de outro loop percorrendo todos os elementos do array. Na primeira passada tem 14 comparações. Na próxima passada, o <code>i</code> vai ser 1 então total menos 1 menos 1 são 13 comparações. Na passada seguinte o <code>i</code> vai ser 2 então total menos 2 menos 1 são 12 comparações e vamos assim até o <code>i</code> ser igual a <code>14</code> e acabar.</p>
<p>Ou seja, vamos ter 14 mais 13 mais 12 mais 11 e assim por diante, que vai dar 105 comparações. Se colocarmos um contador no começo do código e ir incrementando dentro do segundo loop, no final podemos imprimir e ver que vai dar 105. Pra somar todos os número de 0 até N precisamos da matemática de análise combinatória. A fórmula é n vezes n menos 1 dividido por dois. Então 15 vezes 14 dividido por 2 é exatamente 105. Sendo essa fórmula da ordem de grandeza de N vezes N ou seja N ao quadrado, também dizemos que esse algoritmo tem complexidade quadrática.</p>
<p>Complexidade de tempo de um algoritmo é uma das coisas que você mais vai ver quando se compara algoritmos que fazem a mesma coisa. Por exemplo, no caso de ordenação de arrays, temos esse simples que é o Bubblesort e na média tem complexidade quadrático que escrevemos na notação big-O como sendo O n ao quadrado. Outros algoritmos melhores como o Mergesort ou Quicksort, na média tem complexidade O n vezes log de n ou logarítmico. E aqui eu vou falar de complexidade sem ser formal, então se você é matemático perdoe minha nomenclatura mais solta.</p>
<p>Complexidade é outro aspecto da matéria que vocês precisam entender. E pra resumir bem curto existem gráficos como este que compara os principais tipos. Se não me engano o pior tipo seria fatorial, que nem aparece aqui. No caso do nosso exemplo de 15 elementos, se nosso algoritmo fosse extremamente mal feito onde a ordem de grandeza fosse o fatorial de 15, o número de operações seria de 15 vezes 14 vezes 13 vezes 12 etc e isso dá mais de 1.3 trilhão de operações. Comparado com as 105 do bubblesort, que já é ruim, seria totalmente inviável.</p>
<p>Um exemplo de um algoritmo fatorial? É o próprio código da forma recursiva ingênua que calcula o resultado de um fatorial, que você encontra de exemplo em tudo que é lugar. Outro problema famoso que você aprende na faculdade é o do vendedor viajante, “the travelling salesman” resolvido usando força bruta. Tem até de fato um algoritmo de ordenação que de propósito foi feito pra demonstrar um caso fatorial, que é o snailsort, que pra cada elemento ele faz uma permutação de todos os outros elementos. Tudo que envolve permutações que são combinações de todos os elementos com todos os elementos, têm chances de ser fatorial.</p>
<p>Aliás, o algoritmo do vendedor viajante é o problema do sistema de logística de qualquer Amazon da vida. O caminho mais curto pra passar entre diversos endereços ou cidades diferentes. O jeito mais ingênuo de fazer isso, via força bruta, tem complexidade fatorial, ou seja, é absurdamente caro de processar porque a força bruta seria fazer a permutação de todas as possibilidades de rotas entre todos os pontos de destino e no final comparar pra achar a mais curta. E um algoritmo um pouco melhor vai cair de complexidade fatorial pra exponencial, que continua sendo lento.</p>
<p>Esse em particular é da categoria de problemas considerados NP Hard. Problemas ao redor de NP ou “tempo não polinomial” é uma coisa que eu mesmo tenho intuição mas não tenho bagagem matemática pra discutir formalmente. Só entenda que existem diversos problemas matemáticos que não tem uma fórmula simples. Pior, custam complexidade fatorial pra resolver.</p>
<p>Existem problemas P, NP, NP Complete, NP Hard. Problemas P podem ser resolvidos tem tempo polinomial, problemas NP são não-determinísticos cujos resultados podem ser verificados em tempo polinomial. Não se preocupe muito com isso no começo, basta saber que isso existe.</p>
<p>Mas voltando ao que nos interessa, acessar uma posição num array é complexidade constante, ou seja, não importa se o array tem 10 elementos ou 10 mil elementos, sabendo a posição, recuperar um valor custa sempre o mesmo tempo. Agora, procurar um elemento numa posição de uma lista ligada é complexidade linear, depende do tamanho da lista. Como não dá pra fazer aritmética de endereço, porque os elementos não são sequenciais um atrás do outro, a gente precisa ir de Node em Node seguindo o endereço pro próximo. Então pra achar o décimo Node de uma Lista Ligada onde só temos o endereço do primeiro Node, precisamos fazer a operação de seguir pro próximo ponteiro 9 vezes.</p>
<p>Pra achar o vigésimo elemento precisamos seguir ponteiros 19 vezes no pior caso, e assim por diante, ou seja, o tempo que vai custar depende da quantidade de elementos, por isso a ordem de grandeza é chamado de linear, que no gráfico de comparação é a linha que cresce igual nos eixos x e eixo y. Imagina carregar um arquivo com palavras, quebrar num array, e procurar uma palavra. Sem saber coisas como busca binária ou qualquer tipo de indexação via hashing ou algo assim, você vai primeiro fazer um loop e vasculhar elemento a elemento até achar o que procura. Isso é mais ou menos linear, lógico depende do que mais você processa, mas estou falando de ordem de grandeza do caso simples.</p>
<p>Agora, na categoria de logarítmico, estão coisas como busca binária, que eu não expliquei e não pretendo detalhar hoje pra não alongar. Mas pensa assim. Em vez de procurar um a um linearmente, você pode usar a estratégia de segmentar a procura quebrando sua lista primeiro ao meio, e recursivamente ir procurando em listas da metade do tamanho e cada pedaço. É meio que a estratégia de dividir pra conquistar. Imagina uma função chamada <code>pesquisa</code>. E dentro dela você quebra a lista em duas e passa cada metade pra mesma função <code>pesquisa</code>, e vai fazendo isso recursivamente. É a base da idéia de busca binária.</p>
<p>Então numa lista de 100 elementos, você quebra em 2. Agora tem a lista de 50. Quebra de novo em 2. Vai ter 25. Quebra de novo. 12. De novo. 6. De novo 3. De novo 1. Se o que procura estiver na primeira metade, talvez você ache em 6 operações em vez de 50. Essa mesma idéia tá por trás de algoritmos como mergesort ou quicksort que são mais rápidos que bubblesort que é complexidade quadrática. Se voltarmos na nossa animação de algoritmos de ordenação, vou mostrar tanto o merge quanto o quick um embaixo do outro.</p>
<p>Viram como eles segmentam a lista? Logaritmo é a operação inversa da exponencial, se uma complexidade exponencial seria 15 ao quadrado que seria ordem de 225 operações, a logaritmica seria o log de 15 na base 2, que daria menos de 4 operações. Complexidade exponencial fica mais caro mais rápido que linear. São opostos no nosso gráfico e você gostaria de escrever código mais próximo da logarítmica do que da exponencial. Mas não é sempre que você tem a sorte de achar algoritmos logaritmicos, mais comum eu acho que é achar loglineares que é linear vezes logaritmico ou O n vezes log n.</p>
<p>Algoritmos loglineares são por exemplo os mergesort e quicksort que mostrei, que embora sejam bem melhores que quadrático ou exponencial, ainda ficam caros mais rápido que puramente linear. Pra memorizar, veja que a notação O n vezes log n é como se fosse a complexidade linear que é O n vezes a complexidade logaritmica que é O log n.</p>
<p>Essa notação de O alguma coisa ou Big O provavelmente porque usamos uma letra <code>O</code> maiúscula é pra você ter noção da ordem de grandeza do tempo de processamento e/ou do uso de recursos de um algoritmo. Algoritmos podem ser qualquer código que você faz. Tudo que recebe algum dado de input e faz algum processamento pra resultar em alguma coisa. A notação é pra dar uma ideia da complexidade, no pior caso. Por exemplo, o quicksort na média é loglinear que é O n vezes log de n, mas no seu pior caso pode virar quadrático.</p>
<p>Por exemplo, pra acessar o valor de um array, eu repeti dezenas de vezes que é só pegar o endereço do primeiro elemento e somar pelo número da posição vezes o tamanho de bits de cada elemento. Num array de inteiros 64-bits de 100 posições, pra pegar o valor na nonagésima posição basta pegar o endereço da posição 0 e somar por oitenta e nove vezes 8 bytes que é o comprimento de inteiros de 64-bits. Pra qualquer outra posição é o mesmo cálculo.</p>
<p>Independente se o array tiver 10 elementos, 100 elementos ou 100 milhões de elementos, pra pegar o valor em qualquer posição o tempo gasto vai ser exatamente o mesmo, porque esse cálculo de endereço inicial mais a soma da multiplicação entre o número da posição e o tamanho de cada elemento independe do tamanho total do array. É o que em notação de Big O a gente chamaria de O 1, ou seja, tempo constante. Esse é o ideal, e todo algoritmo que chega perto disso pode ser considerado muito bom. Você vê no gráfico que à medida que o tempo corre pra direita no eixo x, a complexidade permanece constante no eixo vertical y, constante.</p>
<p>A Lista Ligada que implementamos tem complexidade O 1, constante, pra operações de inserir novos Nodes ou apagar existentes, porque independe do tamanho total da lista. Mesma coisa pro Array enquanto ele tem espaço sobrando no fim. Mas procurar elementos na Lista Ligada é da ordem de grandeza de O n, linear. No melhor caso, o elemento que queremos é o primeiro, daí só com uma comparação e achamos, no pior caso é o último elemento da lista, daí precisa navegar Node a Node até o fim.</p>
<p>Por isso existe a opção de Lista Duplamente Ligada, onde cada Node aponta pro próximo elemento e pro anterior ao mesmo tempo, assim dá pra começar uma procura do começo da lista ou do fim da lista. Mesmo assim, a complexidade, a ordem de grandeza, continua sendo linear, dependente da quantidade de elementos na lista, sendo o pior caso se o elemento que procuramos estiver no meio da lista, porque se começar a procurar do começo ou do fim, o meio é o ponto mais longe agora.</p>
<p>No hashtable que implementamos a complexidade depende do algoritmo de hashing. Se o hashing for bom, ou seja, resulta em números com pouca colisão, o tempo de inserção no melhor caso seria O 1 constante, porque por baixo é literalmente inserir num array. O pior caso é se o hashing der sempre o mesmo número pra toda chave. Por exemplo, digamos que queremos inserir 3 palavras quaisquer e o hashing de todas dá o número 10, agora todos vão estar na posição 10 do array. E a gente implementou cada elemento pra ser uma lista ligada. O tempo de inserção vira linear, porque tem que chegar até o fim da lista pra adicionar o Node novo.</p>
<p>Mas a pesquisa, que num array seria tempo constante, no hashtable vai ser linear, igual da lista ligada, O n, linear. Outra coisa importante, só porque dois algoritmos tem complexidade Big O iguais, digamos, O n, linear, não quer dizer que elas têm exatamente a mesma performance. Só significa que vão ficando mais lentas linearmente em proporção à quantidade de elementos pra processar. Ambos array e lista ligada tem complexidade O 1 constante pra inserir novos elementos, mas inserir num array é mais rápido porque já tem o espaço reservado pro elemento ao contrário da lista ligada que precisa chamar <code>malloc</code> pra reservar um espaço no heap pra cada elemento. Mas proporcionalmente o tempo de cada um é constante, independente de quantos elementos cada um tenha, entenderam?</p>
<p>O importante é lembrar que tudo que você codifica é um algoritmo (ou mais corretamente, pode ser uma heurística, mas vamos deixar isso pra outro dia). Todo algoritmo tem uma complexidade. Ela tem melhor caso, pior caso e tempo médio. No melhor caso, todas são parecidas. Se você testar um bubblesort, quicksort ou mergesort ou outros como insertion sort ou timsort, num array com 10 elementos, meio que não faz diferença nenhuma.</p>
<p>Você só vai sentir diferença quando começar a passar mil elementos, cem mil, 1 milhão, daí a diferença de tempo começa a ser mensurável e significativa. Isso é pra lembrar que só porque o código rodou na sua máquina, testando com pouca coisa, nem sempre vai funcionar pra todos os casos. Mas sem entender sobre complexidade, só vai entender como seu código é ruim quando estiver rodando em produção com dados reais, e vai ficar coçando a cabeça sem entender porque e vai ficar botando a culpa na linguagem ou no framework.</p>
<p>Seu código roda em tempos diferentes dependendo dos dados que você passar. Por exemplo, o melhor caso de um bubblesort é tentar ordenar uma lista que já tá ordenada. Ele não vai fazer swap de nenhum elemento e ao terminar de comparar com o último elemento, termina. Então vai rodar como se fosse O n, linear. Mas ironicamente, num quicksort, o pior caso é justamente passar uma lista que já tá ordenada, porque ele vai tentar ordenar e rodar em tempo quadrático em vez de loglinear. Ou seja, só porque no tempo médio o quicksort é bom, não quer dizer que é sempre bom.</p>
<p>Por isso mesmo, em linguagens como Java, Javascript, Python e outros, quando você dá <code>sort</code> não necessariamente vai ser um quicksort por baixo. No caso de Python se usa o algoritmo Timsort que é meio inspirado no mergesort com insertion sort que eu não expliquei. É mais estável e com pior caso melhor do que o quicksort. Vale a pena pesquisar. Mas o recado é que nem sempre o algoritmo mais rápido vai ser o melhor sempre, porque se o pior caso derrubar seu sistema, é melhor um algoritmo um pouco mais devagar mas que lide melhor com o pior caso.</p>
<p>Finalmente, antes que alguém jogue nos comentários que eu não mencionei, notação Big O é pode ficar controverso porque povo diz &ldquo;ah, algoritmo X é quadrático então é ruim, algoritmo Y é linear então é bom&rdquo; mas não entende que não é pra comparar só pelo Big O, especialmente sem levar em conta coisas como melhor ou pior caso, e sem levar em conta coisas como Big Omega e Big Theta. Na prática, Big O é uma notação de complexidade do limite superior, não necessariamente do pior caso. Big Omega é notação pro limite inferior, não necessariamente do melhor caso. E Big Theta é notação pro tempo entre Big O e Big Omega.</p>
<p>Se pareceu complicado é porque é um pouco mesmo, na prática Big Omega e Big Theta é mais interessante pro povo acadêmico, mais matemático da ciência da computação. Big O serve pra dar uma noção da ordem de grandeza do tempo de execução de um código ou algoritmo qualquer. Um programador não se preocupa muito com a notação formal matemática, basta entender que existe, e que algoritmos diferentes tem complexidades diferentes. Todo código é diferente, só porque códigos diferentes chegam no mesmo resultado não significa que são equivalentes. Certamente suas complexidades são diferentes. E certamente sua primeira tentativa vai ter complexidade quadrática, então toma cuidado.</p>
<p>Por agora vou encerrar essa introdução à algoritmos e estruturas de dados. E esses dois episódios foram de fato só o topo do iceberg. Mas pros próximos episódios eu precisava que pelo menos todo mundo estivesse na mesma página da existência desses conceitos e uma pequena noção do básico pra eu poder falar de outras coisas. Lembrem-se, o que eu mostrei nesses dois episódios não é um tutorial, nem um curso, pra isso tem material bem melhor. A ideia foi só mostrar que essas coisas existem pra vocês estudarem em mais detalhes.</p>
<p>Eu mesmo não sou acadêmico então me falta a matemática formal. Mas as noções principais incluem que todo algoritmo tem características diferentes. Tem o caso médio, tem os extremos, tem casos excepcionais e cada caso vai rodar com qualidades diferentes de performance e uso de recursos e por isso a gente fala que pra escolher um algoritmo, uma biblioteca, ou mesmo um framework ou arquitetura depende do problema que queremos resolver, como vai ser usado, porque aí uma solução que é boa pra um projeto pode não ser tão bom pra outro. Como já disse no episódio do livro The Mythical Man Month, não existe bala de prata e boa parte do trabalho de um programador é saber como escolher entre diferentes opções.</p>
<p>Quando eu tava na faculdade estudei essa matéria usando o livro de algoritmos do Nicolau Wirth, criador de linguagens como Modula e Pascal, então eu aprendi usando Pascal. Ela é bem simples de ler de cabo a rabo mas eu acho que pra hoje em dia tá defasado. De longe a melhor coleção é o The Art of Computer Programming, escrito por um dos maiores mitos da ciência da computação, Donald Knuth. Ele começou a escrever em 1968 um conjunto que era pra ser de 7 livros. Pausou no fim da década de 70 porque acho que tava trabalhando em outro projeto que tornou ele famoso, o sistema TeX que você deve conhecer como Latex se já escreveu algum artigo científico pra faculdade.</p>
<p>Ainda faltam 3 volumes e os 4 que já existem são bem difíceis de ler e entender sem ter uma base matemática forte. Eu mesmo não tenho paciência e só li trechos específicos. Talvez uma hora faça um episódio de alguns assuntos dele que me interessam. No geral, eu não recomendo esses livros. Pra começar são bem caros. A coleção dos 4 livros na Amazon custa 200 dólares, que convertendo hoje vai pra mais de mil reais. Qualquer livro que você achar que seja de estruturas de dados e algoritmos é suficiente. Veja a bibliografia de qualquer universidade pra ter referências.</p>
<p>Um que não é tão barato, melhor do que eu usei na faculdade, mas tá longe de ser o monstro que é o do Knuth, procure pelo Introduction to Algorithms do Cormen, Leiserson, Rivest e Stein. Diferente do que eu fiz neste vídeo, o livro do Cormen vai começar ensinando o Insertion Sort, no capítulo seguinte vai falar sobre notação assintótica que foi o que eu pulei quando falei sobre notação Big O e na sequência vai explicar o que é a estratégia de dividir pra conquistar e depois um pouco sobre análise probabilística.</p>
<p>Isso é só a introdução. Depois disso ele vai falar de algoritmos de ordenação como Heap Sort e Quicksort, depois estruturas de dados, grafos e só no final vai falar sobre NP Completeness. Eu também não li esse livro de cabo a rabo, mas se você puder, vai cobrir bastante coisa importante. Mesmo não estando no mesmo nível do Knuth, continua sendo um pouco mais pesado na parte matemática mas também com assuntos mais modernos como multithreading.</p>
<p>Mas como eu disse, não importa muito qual livro, contanto que estude pelo menos um que cubra esses tópicos principais de algoritmos, complexidade, estruturas de dados e entenda o que são algoritmos de tempo polinomial versus de tempo não polinomial. Isso é o que em vários vídeos eu venho falando que é parte da base fundamental da programação. Enquanto você não souber esse básico vai ter dificuldade de pular pra tópicos mais avançados e sempre vai ter dúvida se o que tá codando é o melhor jeito pro problema que tem que resolver, ou porque escolher biblioteca X em vez da Y ou porque certa boa prática funciona em alguns casos mas não em outros.</p>
<p>O estudo da ciência da computação é bem divertido quando você começa a sentir que tá dominando a ferramenta e não a ferramenta te dominando, e conquistar esse nível de controle vai exigir centenas de horas de esforço, estudo e exercícios. E esse estudo precisa dessa base. Espero que estes vídeos tenham dado um pouco de luz e vocês se motivem a procurar o resto sozinhos. Se ficaram com dúvidas mande nos comentários, quem já estudou tudo isso ajudem os colegas também, se curtiram o video deixem um joinha, assinem o canal, não deixem de clicar no sininho pra não perder os próximos episódios e continuem compartilhando com seus amigos pra ajudar o canal. A gente se vê, até mais.</p>
]]></content:encoded><category>algoritmos-e-estruturas-de-dados</category><category>ciencia-da-computacao</category><category>programacao</category><category>akitando</category></item><item><title>[Akitando] #93 - Hello World Como Você Nunca Viu! | Entendendo C</title><link>https://akitaonrails.github.io/2021/03/06/akitando-93-hello-world-como-voce-nunca-viu-entendendo-c/</link><guid isPermaLink="true">https://akitaonrails.github.io/2021/03/06/akitando-93-hello-world-como-voce-nunca-viu-entendendo-c/</guid><pubDate>Sat, 06 Mar 2021 10:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/Gp2m8ZuXoPg"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Se você nunca viu C ou como um programa funciona de verdade no baixo nível, hoje é sua chance de ver todo o básico de uma só vez!&lt;/p&gt;
&lt;p&gt;Vou desde tipos primitivos, strings, arrays, stacks, heap, alocação de memória, structs, até minimamente entender o que está por baixo do que você chama de linguagem &amp;ldquo;orientada a objetos&amp;rdquo;.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/Gp2m8ZuXoPg"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você nunca viu C ou como um programa funciona de verdade no baixo nível, hoje é sua chance de ver todo o básico de uma só vez!</p>
<p>Vou desde tipos primitivos, strings, arrays, stacks, heap, alocação de memória, structs, até minimamente entender o que está por baixo do que você chama de linguagem &ldquo;orientada a objetos&rdquo;.</p>
<p>ERRATAS</p>
<ul>
<li>em 00:06:50 falei errado o range de INT, o certo é de -128 a 127</li>
<li>em 00:06:06 eu falei certo e deixei a correção escrita errada. 64 bits, se você não precisar, desperdiça 7 bytes</li>
<li>em 00:40:54 eu falei que 255 bytes é 1/4 de 1 megabyte, mas é de 1 KILObyte.</li>
<li>em 00:45:41 eu coloquei <code>hello</code> no sizeof e ele pegou o sizeof do endereço e não do array.</li>
<li>em 00:38:40 eu não sei como deixei passar, mas quando falo de passar o string pras funções <code>f1</code>, <code>f2</code> não está duplicando toda a string e sim a referência pra ela. Se fossem valores primitivos como <code>int</code> sim, mas array só o endereço duplica mesmo.</li>
<li>em 00:57:55 quando dei copy e paste pro createPerson, eu esqueci de usar os argumentos pra fazer <code>person.age = age</code> por exemplo e ficou hardcoded. Viram?? Por isso copy e paste é perigoso, especialmente meia noite depois de estar cansado de passar 3 dias editando kkkkk</li>
</ul>
<p>LINKS:</p>
<ul>
<li>Integer (Wikipedia) (<a href="https://en.wikipedia.org/wiki/Integer_%28computer_science%29"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Integer_(computer_science)</a>)</li>
<li>Two’s Complement (Wikipedia) (<a href="https://en.wikipedia.org/wiki/Two%27s_complement"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Two%27s_complement</a>)</li>
<li>How numbers are encoded in JavaScript (<a href="https://2ality.com/2012/04/number-encoding.html#:~:text=JavaScript%20numbers%20are%20all%20floating,binary%20format%2C%20in%2064%20bits"target="_blank" rel="noopener">https://2ality.com/2012/04/number-encoding.html#:~:text=JavaScript%20numbers%20are%20all%20floating,binary%20format%2C%20in%2064%20bits</a>.)</li>
<li>FLOATING POINT VISUALLY EXPLAINED (<a href="https://fabiensanglard.net/floating_point_visually_explained/"target="_blank" rel="noopener">https://fabiensanglard.net/floating_point_visually_explained/</a>)</li>
<li>What Every Computer Scientist Should Know About Floating-Point Arithmetic (What Every Computer Scientist Should Know About Floating-Point Arithmetic (oracle.com))</li>
<li>IEEE-754 Floating Point Converter (IEEE-754 Floating Point Converter (h-schmidt.net))</li>
<li>Number.MAX_SAFE_INTEGER (Number.MAX_SAFE_INTEGER - JavaScript | MDN (mozilla.org))</li>
<li>Signed Binary/Decimal Conversion Using the Two&rsquo;s Complement Representation (Signed Binary/Decimal Conversion (ubc.ca))</li>
<li>C - Pointer arithmetic (C - Pointer arithmetic - Tutorialspoint)</li>
<li>Why Discord is switching from Go to Rust (<a href="https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f"target="_blank" rel="noopener">https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Seguindo meus episódios com o tema de fita de bits que venho explicando desde os episódios de introdução à computação até a história de Turing e Von Neumann, no penúltimo episódio eu expliquei sobre a diferença entre arquivos binários executáveis como os de formato ELF e arquivos texto e como tudo são fitas de bits ou o que eu chamei de linguição de bits. E hoje quero tentar explicar como podemos representar dados e manipular bits nessa linguiçona pra vocês conseguirem enxergar um pouco melhor como linguagens de programação lidam com dados.</p>
<p>Se você foi direto aprender uma linguagem de alto nível como Python ou Javascript, sem ter aprendido linguagens de baixo nível, vai eternamente ter dúvidas de &ldquo;o que&rdquo; exatamente a linguagem tá fazendo. Por que certos comportamentos estranhos acontecem do nada. Eu aprendi programação no começo dos anos 90 com linguagens de alto nível também, no caso foi Basic e depois dBase. Eu só fui entender como as coisas funcionavam quando comecei a estudar Ciências da Computação. Muita gente resolveu pular essa etapa e por isso vai ficar travado muito em breve. No video de hoje eu quero tentar abrir as portas pra coisas que você raramente vai encontrar num curso qualquer por aí.</p>
<p>Infelizmente é impossível eu resumir anos de ciências da computação num video só, então você vai precisar complementar o que eu disser hoje com a ajuda do seu professor da faculdade, ou pelo menos alguém mais experiente que você. Aliás, sei que tem vários professores que assistem os vídeos, então este é um episódio onde seus alunos vão precisar de ajuda.</p>
<p>Eu fiquei me indagando se devia quebrar este episódio em dois ou três mas resolvi entuchar tudo em um só mesmo pra tirar isso do caminho, então já aviso que vai ser super denso e bem difícil mesmo se você for iniciante. É normal não entender tudo só assistindo uma vez. Mesmo se você já sabe C, talvez goste de rever alguns conceitos de novo e até ajudar o povo nos comentários. Com tudo isso dito, respirem fundo, peguem papel e caneta pra anotações e vamos lá!</p>
<p>(&hellip;)</p>
<p>O objetivo hoje é destrinchar o linguição de bits de Turing e mapear o que acontece numa linguagem de programação de verdade. Vamos seguir o buraco de coelho e tentar enxergar a mulher de vermelho na Matrix. E a primeira coisa pra relembrar: na fita de bits, a máquina de Turing, ou o tal programa tem tanto binários que representam instruções pra sua CPU como também valores e dados, tudo junto um atrás do outro. Por exemplo, num hello world da vida, vai existir o valor &ldquo;hello world&rdquo; como uma cadeia de caracteres, o que chamamos de um String, e junto vai ter instruções pra mostrar esse String na tela de alguma forma.</p>
<p>String, em inglês é tipo um barbante, uma corda ou um fio. A forma mais simples de String é uma cadeia de bytes terminada com nulo de C. Se a gente encodar o &ldquo;hello world&rdquo; usando ASCII ou UTF-8, cada letra será representada por um código de 8-bits que é 1 byte. Então serão 11 bytes de tamanho mais o nulo no final. Mas se for em japonês &ldquo;haroo waarudo&rdquo;, isso é uma String de 7 símbolos, cada símbolo no alfabeto katakana vai ocupar 3-bytes, então serão 21 bytes de tamanho. Como expliquei no episódio anterior, quantos bytes cada símbolo ocupa depende da tabela de conversão entre glifos e caracteres. Esse é o encoding.</p>
<p>Uma String com caracteres ocidentais de 8-bits é particularmente interessante porque é igual a um Array de bytes. Array em inglês é tipo uma série ou conjunto de coisas. Uma lista sequencial, com cada elemento do array um atrás do outro na memória, sem buracos no meio. Essa distinção é importante e já vou voltar a falar de arrays daqui a pouco, mas antes precisamos falar de números.</p>
<p>Em algumas linguagens você vai ver duas palavras distintas, um byte e um char. Se o char for ASCII de 8-bits, então um char é a mesma coisa que um byte na prática, como é em C. Mas em C# ou Java cada char representa um caractere UTF-16 que tem 16-bits ou 2 bytes, portanto é um double byte. Não se pode achar que um char é sempre um pra um com um byte, só quando falamos de C. Como hoje só vou falar de C, então considere um char sendo 1 byte.</p>
<p>Cada linguagem suporta um certo número do que chamamos de tipos primitivos. Char é um exemplo de primitivo, que é um pequeno pacote de bits, sem metadados. Outro exemplo são números. No mundo real estamos acostumados a pensar em números como os inteiros indo de menos infinito até mais infinito, mas em computadores os números são limitados por seus tipos e temos diversos tipos de inteiros.</p>
<p>Na maioria das linguagens de programação a gente costuma lidar com Inteiros de 8, 16, 32 ou 64-bits, respectivamente Int8, Int16, Int32 ou Int64. Na linguagem Rust, por exemplo, é exatamente assim que é dividido e eles são chamados de i8, i16, i32 e i64. E o Rust ainda tem o i128 também. Um int8 é basicamente um byte ou um char de C.</p>
<p>No caso de C# o Int16 é chamado de Short, o Int32 é o Inteiro propriamente dito e Int64 é chamado de long. No caso do C é mais complicado porque se eu não estou muito errado o int dependia da arquitetura do CPU, então um int num computador de 16-bits era o Int16, mas num computador de 32-bits, se você recompilasse o mesmo programa, o int passava a ser 32-bits.</p>
<p>À primeira vista parece uma boa idéia, porque assim toda vez que as CPUs evoluem, bastaria recompilar o programa e ele automaticamente teria acesso a mais memória. Mas na prática isso pode levar a diversos tipos de bugs inesperados. Então quem decide o tamanho dos inteiros de C é o compilador. Mas nas linguagens mais modernas como C# ou Rust a declaração fica mais explícito qual tipo de inteiro você tá usando.</p>
<p>A razão de ter diferentes tipos de inteiros é a eficiência. Se você sabe que no seu programinha não vai precisar de números maiores que duzentos e cinquenta cinco, basta alocar um int8, ou 1 byte, e vai ser suficiente. Se você alocar um int64 sem precisar, vai desperdiçar 7 bytes que não vão servir pra nada. Se for uma variável só, não faz diferença, mas se for um array com milhares ou milhões de elementos, você vai rapidamente desperdiçar megabytes ou até gigabytes à toa.</p>
<p>Números inteiros ainda tem outra característica importante. Eles podem ser signed ou unsigned, ou seja, ter sinal ou não ter sinal. Se declarar um inteiro unsigned, sem sinal, ele pode ser usado pra números de 0 até hexa FF que é duzentos e cinquenta e cinco. Mas se for signed pode ir de negativo 127 até positivo 128. Quase a metade. Nesse caso o primeiro bit, que é chamado de bit mais significativo, e costuma ficar à esquerda dependendo do endianness do processador, é onde vai ficar o sinal. Se o primeiro bit for 0 então é um número positivo. Se for 1, então é negativo.</p>
<p>Daria pra fazer um video inteiro só falando de inteiros negativos e porque eles são interessantes, e isso é uma das coisas que recomendo pesquisarem a respeito depois. Mas vamos dizer o seguinte. Pra subtrair dois números com sinal, como 15 e 5, o computador primeiro vai enxergar eles em binário. Em 8-bits, no caso de 15 seria o binário 0000 1111. No caso de 5 seria 0000 0101. Você já aprendeu que subtrair um número é o mesmo que somar com seu negativo. Então poderíamos usar a máquina de somar que já mostrei como funciona no episódio de introdução mais hardcore a computadores. Reveja se não lembra mais.</p>
<p>Computadores usam o que se chama de método de complemento, que é a ideia de fazer uma subtração virar uma soma e usar o mesmo circuito de soma. Pra fazer isso ele transforma o segundo número no seu complemento de dois, ou two’s complement. Esse é um tema que você pode perguntar pro seu professor se ele não explicou. Pra achar esse complemento é simples, basta flipar todos os bits e somar 1, então o número 5 que é 0000 0101 vai virar 1111 1010 + 1 que é 1111 1011. O porquê de ter esse 1 extra no final é um dos motivos pra você estudar a teoria completa de two’s complement depois. Por agora vamos somar o 15 com esse 5 negativo.</p>
<p>Da direita pra esquerda, vamos lá, 1 mais 1 dá 2 que é 1 e 0 então fica zero e sobe um. Agora 1 mais 1 mais 1 é 3 que é 1 e 1, então fica 1 e sobe 1 de novo. 1 + 1 é 1 e 0 então fica zero e sobe um. De novo 1 mais 1 mais 1 é 3 então fica 1 e sobe 1. Agora tá fácil, 1 mais 1 fica zero e sobe 1. De novo, 1 mais 1 fica zero e sobe 1. Fica zero e sobe um. Fica zero e sobe um.</p>
<p>O último um que subiu a gente descarta. E o que fica no final é 0000 ou seja, como o primeiro bit é zero, o número é positivo e depois 1010 que é 8 mais 2 que dá 10. Exatamente 15 menos 5, como você já sabia. E com isso demonstramos que é possível subtrair números inteiros com sinal usando o método de complementos e reusando o mesmo circuito da máquina de somar binário.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>11111 111
 0000 1111
&#43;1111 1011
 ---------
         0
        1
       0
      1
     0
    0
   0
  0</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As CPUs são feitas de tal maneira pra tirar vantagem de ser base 2 em vez de base 10 pra conseguir calcular de forma mais eficiente do que fazemos com número decimais. Multiplicar e dividir é mais complicado e se tiverem interesse estudem o algoritmo de multiplicação de Booth. Mas tudo isso foi só pra dizer que existe diferença entre inteiros com sinal e sem sinal.</p>
<p>Já expliquei que dentro da CPU existem diversos registradores, que é como se fossem variáveis globais fixas no hardware. No 6502 do Nintendinho tinha o registrador A, X, Y e outros, todos de 8-bits. E quando falamos de um Intel ou AMD moderno, eles têm registradores e instruções de 64-bits. Mas e quando precisamos fazer contas com números extraordinariamente grandes, acima dos exabytes que 64-bits comportam? Isso não é incomum, principalmente num mundo de Big Data e data science. Pra isso existem tipos compostos como o BigInteger, que tecnicamente não tem limites.</p>
<p>Se você tentar colocar um número de 128-bits num registrador de 64-bits vai dar overflow, ou seja, transbordar. Daí esse número seria truncado e metade seria perdido. Em vez disso, alocamos memória na RAM pra guardar esse número e, a grosso modo, fazemos a soma com a primeira metade, depois com a segunda metade e concatenamos o resultado. E sim, você pode fazer a soma de um número grande como duas somas de números menores, basta carregar o carry bit pra segunda soma.</p>
<p>Então precisaria de pelo menos 2 instruções de adição numa máquina de 64-bits pra somar dois BigInteger de 128-bits, mas é possível. Só vai custar pelo menos 2 vezes mais caro do que somar números menores de 64-bits. Tudo em computação tem um custo, não é automático nem tudo igual.</p>
<p>Isso dito, falei um monte de nomes que podem parecer complicados mas só precisa lembrar que são convenções de como representar números inteiros decimais em formato binário, com limites que vão de 8 até 64-bits dependendo da arquitetura nativa da CPU, ou mais se usarmos abstrações como BigInteger. E com esses números podemos fazer praticamente tudo que precisamos. Aliás, pense que a geração de microcomputadores de 8 e 16 bits foi quase inteira baseada só em números inteiros. Todo joguinho de nintendinho até mega drive é feito usando números inteiros.</p>
<p>A tela de um jogo tem 320 colunas por 240 linhas de resolução, que é um número inteiro. Um sprite de nintendinho tem 8 por 8 pixels ou 8 por 16 pixels. Pra mover um sprite de lugar basta ir somando de um em um pixel. Os sprites podem escolher entre paletas de 4 cores dentre 54 disponíveis. Pontuação de jogos, quantidade de vidas, número de fases, são todos números inteiros.</p>
<p>E fora dos games, em aplicativos de produtividade, seja planilhas, processadores de texto, tudo é baseado em números inteiros. Quantidade de letras ou palavras num texto é um número inteiro. Mesmo se você mexer com coisas como preços, que têm frações da moeda em centavos, basta multiplicar por 100 e fazer contas com inteiros e só no final dividir por 100 e truncar depois de 2 casas decimais. Então preços de produtos, salários, impostos, tudo pode ser calculado facilmente e mais importante, rápido, porque o hardware tem circuitos pra somar números inteiros.</p>
<p>Foi principalmente com o advento de coisas como computação gráfica, animação em 3D, que números fracionados começaram a fazer diferença no mundo comercial. Claro, na computação científica já precisavam fazer cálculos fracionados com muita precisão. Em previsões de meteorologia ou laboratórios. Mas isso era em nichos mais isolados e restritos com acesso a hardware especializado. Desde o começo do século XX já haviam formas de calcular números fracionados. O Z1 de Konrad Zuse, que mencionei na história de Turing e Von Neumann, já tinha esses conceitos. Inclusive seu Z4 foi o primeiro computador comercial nos anos 1940 a ter suporte a números fracionados.</p>
<p>Mainframes como o UNIVAC ou os da IBM dos anos 60 tinham suporte a calcular números fracionados também. Mas foi só quando começamos a adotar arquitetura de 32-bits no começo dos anos 80 que os diversos padrões começaram a convergir pro que viria a se tornar o padrão IEEE 754. E só a partir do meio dos anos 80 que começamos a adotar em microcomputadores domésticos.</p>
<p>Nos CPUs de 8-bits como o 8086 a Intel começou a desenvolver o que se chamaria de co-processadores matemáticos, como o 8087. Até a era do 386 a gente tinha opção de adicionar um segundo chip, como o 80387 que custava caro pra caramba e poucos programas usavam. Não lembro se foi já a partir dos 486 ou só nos Pentiums que paramos de precisar de co-processadores porque as instruções passaram a vir na própria CPU.</p>
<p>Precisamos entender pra que serviam esses co-processadores. Eu disse que até agora estávamos indo bem com números inteiros. Os CPUs dos anos 80 eram bons em processar números inteiros. Mas eventualmente a gente precisa lidar com números fracionados, com decimais. Em particular, números irracionais, ou seja, números que não podem ser representados por uma fração de números inteiros. Raíz quadrada de 2, o número Pi e muito mais. O Pi é o inteiro 3, depois tem uma vírgula e começa uma cadeia infinita de números um, quatro, um, cinco, nove, dois, seis, cinco, três e assim vai sem padrão nenhum até o infinito.</p>
<p>Se um número com decimais pode ser representado por uma fração, podemos usar números inteiros. Por exemplo, o número 0 vírgula 33333333 ad infinitum, é o inteiro um dividido pelo inteiro três. Assim muitos números com casas decimais infinitas podem ser representadas claramente com números inteiros finitos. Mas o mesmo não acontece com números irracionais como Pi. E computadores não tem memória infinita, então não temos como representar perfeito. Daí precisamos de uma aproximação. E a palavra importante aqui é “aproximação”, o que significa que ele não vai ser armazenado na memória como você acha que deveria e sim algo perto disso.</p>
<p>Essa aproximação é a especificação IEEE 754 que define o que chamamos de ponto flutuante. Esse tema é super chatinho e levaria um curso inteiro pra explicar e mesmo assim você vai ter dificuldade de entender, então vou simplificar grosseiramente hoje. Agora pouco eu expliquei como podemos ter inteiros com sinal e sem sinal. E quando tem sinal reservamos o primeiro bit mais significativo pra ser o sinal. Se for zero é positivo e se for 1 é negativo. Mas podemos reservar mais bits pra outras coisas também. Bits são o que a gente quiser que elas sejam.</p>
<p>Em países como Brasil as casas decimais são separadas por vírgula, mas nos Estados Unidos e em outros lugares se usa ponto em vez de vírgula. Por isso falamos em ponto flutuante e não vírgula flutuante. Você já deve ter ouvido falar do tipo <code>float</code>. O ponto separa a parte inteira do número da parte decimal e num computador fazemos esse ponto flutuar pra aumentar ou diminuir a precisão. Mas isso não é a história toda.</p>
<p>O IEEE 754 define vários tipos de float. O mais conhecido hoje é o binary64 que é um número de 64-bits ou 4 bytes. O primeiro bit é igual de um inteiro com sinal, ele define o sinal. Depois vêm 11 bits que representam o número do expoente e os demais 52 bits representam a mantissa. E o que é isso? É notação científica, mas na base 2. Na base 10 pra representar um número como 15000 poderia ser 1.5 x 10^4 ou 1.5e4, e esse “e” é de “expoente”. 1.5 é a mantissa e 4 é o expoente na base 10.</p>
<p>Um float é a mesma coisa, só que seria o primeiro bit de sinal pra positivo ou negativo, daí 2 elevado ao expoente armazenado nos próximos 11 bits, multiplicado pelo número representado nos 52-bits seguintes, que é a mantissa. O número inteiro máximo que pode ser representado nesse formato é 2 elevado a 53 que dá o número 9 quadrilhões e alguma coisa, que em particular, se você é de Javascript, é o que você já conhece como MAX_SAFE_INTEGER que, em binário, é representado assim aqui do lado: zero pro sinal pra ser positivo, o expoente só com o último bit flipado e a mantissa inteira de uns.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>0
00000000001
1111111111111111111111111111111111111111111111111111</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Da mesma forma, o que no Javascript você chama de MIN_SAFE_INTEGER que é o menor número que se pode representar assim, é só flipar ou inverter todos os bits do expoente e o primeiro bit significativo do sinal que temos o 9 quadrilhões e tanto negativo.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>1
11111111110
0000000000000000000000000000000000000000000000</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O grande lance é que não conseguimos representar em notação científica de base 2 exatamente o número que normalmente queremos de decimal, sempre vai ter um erro de conversão. Por exemplo, podemos ir no site do link abaixo, que eu vou deixar nas descrições, que mostra essa conversão pra gente. Se tentarmos converter 0.1 na realidade vai dar zero ponto um zero zero sete vezes um quatro nove etc que é esse binário aqui do lado. É uma aproximação na base 2. Ou seja, depois do 1 vai ter uma pequena sujeira por isso tecnicamente é diferente de 0.1 exato, embora bem próximo se arredondar.</p>
<p>Mesma coisa se tentarmos converter 0.2 em binário, vai virar zero ponto dois, zero zero sete vezes de novo, dois nove oito etc. E esse numero float em binário é esse outro numerozão aqui do lado. E pra finalizar, 0.1 mais 0.2 deveria ser 0.3 se fosse base 10, mas 0.3 em binário vai ser esse numerozão aqui do lado. Com o erro de conversão na verdade vai ser zero ponto três zero zero seis vezes um um nove dois etc. Sempre vai sobrar uma sujeirinha em vez de ser zero zero zero só zero.</p>
<p>Se somarmos o 0.1 e 0.2 na instrução de soma binária do CPU, com os erros de precisão da conversão em binário, o resultado vai dar zero ponto três zero zero sete vezes, depois quatro quatro sete etc. Mas a conversão do 0.3 pra float dá zero ponto três zero zero só seis vezes em vez de sete e depois um um e não quatro quatro. É por isso que no Javascript e em diversas outras linguagens como Python, se você fizer 0.1 + 0.2 e tentar comparar com 0.3 vai dar false. Porque de fato quando converter de decimal pra float na base 2 ele vai ganhar essas sujeirinhas de erro de precisão na conversão e quando se faz conta com essas precisões ligeiramente erradas, a precisão do resultado também vai ser ligeiramente errado.</p>
<p><code>0.100000001490116119384765625 + 0.20000000298023223876953125 00111101110011001100110011001101 + 00111110010011001100110011001101 0.3 = 0.300000011920928955078125 = 00111110100110011001100110011010 0.1 + 0.2 = 0.300000004470348358154296875</code></p>
<p>Quem já tentou fazer muitas contas com float já se deparou com isso. E pra ser justo não é só em Javascript. Se você fizer a comparação de 0.1 mais 0.2 com 0.3 vai encontrar que é falso em Ruby, em Rust, em Python. Se não me engano, C# é uma linguagem que se dá ao trabalho de ajustar esse erro de precisão e devolve verdadeiro, mas no resto, que só faz a conversão direta pra binary64 do IEEE 754, vai ter esse problema nada intuitivo se você só pensar na base 10 e não na base 2. Você faz 0.1 mais 0.2 e no final não é exatamente 0.3. E isso é pior no Javascript por outra razão.</p>
<p>Em Javascript o único tipo numérico que existe é o float binary64. Não existem inteiros primitivos como mostrei antes. Então, em vez do inteiro máximo de dois elevado a 64, o máximo vai ser dois elevado a 53 como declarado no tal MAX_SAFE_INTEGER. Que é um número grande, mas não é o máximo que sua arquitetura suporta. E se você fizer conta com dinheiro, tipo desconto em produtos, ou calcular saldo baseado num extrato de conta, não faça contas com float, porque esse erro de precisão uma hora vai dar ruim. Pequenas sujeiras de precisão, em volume grande de contas, uma hora vai afetar bastante. No mínimo multiplique o valor por 100 pra trabalhar com centavos ou trunque depois de duas casas pra não ter essa sujeira final.</p>
<p>Melhor ainda, justamente por conta de bugs de precisão como esse, existe outro tipo de dado. Semelhante ao BigInteger que falei antes, a maioria das linguagens tem um tipo chamado BigDecimal. Assim como o BigInteger, pra fazer contas aritméticas, vai ser mais lento, porque vai precisar quebrar o numero em segmentos, fazer aritmética das partes e depois dar um jeito de concatenar o resultado, porque o numerozão não cabe nos registradores de 64-bits. No caso do BigDecimal, mesma coisa, ele vai permitir mais precisão e cuidar pra evitar essa sujeira de conversão.</p>
<p>A linguagem Javascript não tem um BigDecimal nativo, então você precisa baixar uma biblioteca que ofereça isso. Na verdade esse é um dos pontos onde Javascript é corretamente criticado. Todo número nele, seja um inteiro ou seja um float, em binário, é representado como um float binary64, o que eu pessoalmente acho tosco. Existe uma classe Number e ela deveria converter nos tipos corretos internamente, até pra pesar menos toda vez que precisa fazer cálculos. Dependendo de que versão da história você ver, esse é um dos efeitos colaterais da linguagem ter sido criada tão às pressas. E depois que já foi adotado assim, você tem dificuldade de voltar atrás agora. Pesquise sobre isso depois.</p>
<p>Enfim, o float que a maioria das linguagens usa é o tal binary64, de 64-bits que eu expliquei. Mas antigamente o que era chamado de float em C era a versão binary32. E a versão 64-bits era chamada de double. Então se algum dia você ver double, é o float de 64-bits. De qualquer forma, o importante era explicar que existia essa forma de se representar números e existem outras, como pra representar números complexos, mas não é importante pra hoje. Lógico, que fique de lição de casa pra vocês depois.</p>
<p>Agora vamos voltar ao tal do Array. Array é um conjunto que contém elementos do mesmo tipo, sequencialmente, um atrás do outro na fita de bits, como por exemplo array de números int8 ou float. Quando associamos uma variável a um array na realidade estamos apontando pro endereço do primeiro elemento desse array. E toda vez que tentamos acessar uma posição específica dentro desse array, basta pegar o endereço inicial e somar pelo tamanho do tipo do elemento que ele contém.</p>
<p>O importante de Arrays e Strings é entender que a sequência de bytes ou chars é necessariamente um atrás do outro, sem intervalos no meio. Se a gente aloca um array de 100 elementos int8, que é de 1 byte de tamanho, o array vai usar cento e um bytes de memória contígua. Esse um a mais é o byte nulo no final da sequência que indica o término dela, no caso de Strings. A beleza é que pra acessar qualquer elemento dessa sequência, sabendo a posição, basta pegar o endereço do primeiro elemento, o comprimento de cada elemento, no caso 1 byte, e multiplicar pela posição pra ter o endereço do que estamos procurando.</p>
<p>No exemplo do &ldquo;hello world&rdquo;, é um array de chars de 12 bytes contando o nulo no final. Se eu quiser puxar a 7a letra, basta pegar o endereço do primeiro elemento que é o &ldquo;h&rdquo; do hello, e somar por 7 bytes, já que cada char tem 1 byte de comprimento, e nesse 7o endereço vai estar a letra &ldquo;w&rdquo;, de &ldquo;world&rdquo;, como esperado.</p>
<p>Agora, isso só funciona porque cada elemento tem exatamente o mesmo comprimento, a mesma quantidade de bits. O requerimento de todo array é que todos os elementos dentro devem ser do mesmo tipo. Se quisermos armazenar números long, que tem 4 bytes, então todos os outros elementos precisam ser long. Daí um array de 100 elementos long vai ter 400 bytes de tamanho. E um outro problema disso é que um array tem tamanho fixo. Uma vez alocado, não tem como ser expandido.</p>
<p>Daí você pode ficar confuso, porque num Javascript e outras linguagens, se o array inicial tem 100 elementos e eu quiser adicionar mais, basta acessar direto a posição 101 e escrever lá. Por exemplo, vamos abrir o console de Javascript em qualquer navegador e declarar um array chamado <code>lista</code> com umas cinco letras, como nosso &ldquo;hello&rdquo;. Se eu tentar acessar a posição 4, vamos pegar a última letra &ldquo;o&rdquo;, e é 4 porque todo array que se preza começa da posição 0. Se eu tentar pegar a posição 5, vai vir undefined porque passei do fim do array. Mas eu posso tranquilamente gravar uma nova letra como &ldquo;exclamação&rdquo; nessa posição e boom, o array automaticamente “expande” pra comportar a nova letra, mas como isso é possível se eu falei que arrays tem tamanho fixo?</p>
<p>Na maioria das linguagens mais alto nível que C, tem estratégias, do grego estrategia. Se eu não disser o tamanho exato que quero, ela vai pré-alocar um espaço maior do que preciso. Dessa forma, se precisar de mais elementos depois do fim, tem espaço livre sobrando. E é uma estratégia quando você sabe que tem mais RAM do que precisa. Mas, se eu realmente chegar ao fim do espaço que a linguagem alocou pro meu array, o que ela vai fazer é alocar um novo array maior, copiar elemento a elemento pro novo array e descartar o antigo.</p>
<p>De novo, você não tá sabendo, mas tá acontecendo um custo extra de processamento por baixo dos panos. No dia a dia você nem percebe porque normalmente mexe em poucos arrays, mas em operações que tenham milhares ou milhões, aí você vai querer tomar mais cuidado com operações que fiquem mudando o tamanho dos arrays. Mesmo que a operação na linguagem seja só uma linha, por baixo dos panos ela vai fazer diversas outras, como duplicar seu array, que você nem fica sabendo e aí não entende porque a performance ficou tão ruim sem motivo aparente.</p>
<p>Mais ou menos a mesma coisa acontece com Strings. No C, sem bibliotecas extras, um String é literalmente declarado só como um array de chars. Em linguagens mais modernas o String é um tipo separado do Array, mas internamente a maioria usa Arrays, só que o tipo extra dá um suporte maior a coisas como conversão de encoding e tudo mais. Na prática, tanto arrays quanto strings, uma vez declarados e preenchidos, são fixos. E aumentar o tamanho deles pode envolver criar um novo array e copiar o conteúdo do antigo em cima do novo. Por isso um Java tem classes extras como StringBuffer pra lidar com strings que você vai precisar crescer depois.</p>
<p>Agora, tudo isso que eu vim falando até agora, array, string, ints ou floats, todos ocupam algum lugar numa memória, seja RAM, ROM, SSDs ou qualquer outra coisa. É tudo uma fita de bits, onde cada posição tem um endereço. Zero, um, dois, e assim vai. Cada valor em uso pode ser encontrado nessa fita com um endereço, é uma localização. O conceito importante aqui é entender que tudo tem um endereço nessa fita, que pode ser qualquer tipo de memória. Pode parecer complicado só porque endereços em máquinas de 64-bits podem ir até numerozões do tamanho de exabytes. E pra piorar escrevemos esses endereços como hexadecimais. Mas endereços são só isso: números.</p>
<p>Eu sei que tô andando bem rápido e já soltei bastante coisa até agora, mas se segurem que agora que a coisa vai ficar interessante. Recomendo que depois assistam tudo de novo com calma se isso é novo pra vocês, mas agora é uma boa hora pra eu descer mais ainda na escovação de bits. Uma coisa que não é claro pra iniciantes é onde ficam todas essas variáveis ao longo da execução de um programa. Então vamos partir de um exemplo bem besta em C. E não se preocupem se você nunca usou C, mesmo assim você deve ser capaz de acompanhar.</p>
<p>Em outros episódios eu já mostrei como isso funciona. A gente abre um editor de textos, escreve o codigozinho em C e usa o compilador <code>cc</code> que é o GCC pra ler esse código e dar output ou saída num binário executável em formato ELF, que o Linux consegue entender e executar. Se não sabe o que é isso, pelo menos assista o episódio da diferença de arquivos binário e texto, onde eu mostro como isso funciona.</p>
<p>O binário ELF tem um endereço específico onde fica a primeira instrução que vai começar a executar. No caso é o hexa 0x1000. Esse endereço é gravado no registrador chamado PC, que é o program counter ou contador de programa. É a partir disso que o CPU vai executando a instrução que tá apontado nesse contador. Uma vez executado o contador incrementa o endereço e aponta pra próxima instrução na fita de bits.</p>
<p>Bem a grosso modo, só pra ficar mais fácil de explicar, é como se esses endereços fossem os números da linha do código C no nosso editor de textos. Claro que não é, porque o que tá no binário não é o código que digitamos e sim o que o compilador traduziu pra linguagem de máquina, o linguição de bits. Mas pra facilitar a explicação, pense que no tal endereço 0x1000 ele pula ou dá JUMP pro endereço da função <code>main</code> do C e vai executando uma linha de cada vez dessa função. Sempre que eu me referir a endereço de programa como sendo o número da linha, é só uma metáfora pro endereço hexadecimal da instrução equivalente na memória de verdade.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;

void f2(char hello[]) {
    printf(&#34;from f2: %d\n&#34;, &amp;hello);
    printf(&#34;%s\n&#34;, hello);
}

void f1(char hello[]) {
    printf(&#34;from f1: %d\n&#34;, &amp;hello);
    f2(hello);
}

void main() {

    char hello[] = &#34;Hello World&#34;;
    printf(&#34;from main: %d\n&#34;, &amp;hello);
    f1(hello);

    return;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A função <code>main</code> é o ponto de partida de qualquer programa em C. Neste exemplo vai alocar uma String &ldquo;hello world&rdquo;. Daí pegamos a referência dessa variável com esse &amp; comercial na frente. Isso pega o endereço onde a string tá armazenada pra podermos imprimir na tela. Daí passamos a variável pra outra função que vamos chamar de <code>f1</code> e colocar no começo do arquivo. Aqui parece qualquer outra linguagem que você já usou, chamando uma função e passando um argumento entre parênteses. E precisamos declarar a função <code>f1</code> antes da <code>main</code> porque em C, pra usar uma função embaixo, ela precisa estar definida antes. Em outras linguagens isso não é verdade, depende do compilador. Mas no caso de C é assim.</p>
<p>Daí nessa função <code>f1</code> vamos receber como argumento um array de chars de novo e dentro vamos imprimir o endereço onde a variável desse argumento foi armazenado, e depois repassar essa variável pra outra função <code>f2</code>. De novo, definimos acima a função <code>f2</code> que, mesma coisa, vai receber um argumento de array de chars, imprimir o endereço da variável e por último imprimir o valor da variável que é o &ldquo;hello world&rdquo;. É um exemplo super besta, só fomos passando o mesmo valor algumas funções pra frente. Vamos sair do editor e usar <code>gcc</code> ou <code>cc</code> pra compilar esse código no tal executável ELF, e podemos rodar.</p>
<p>Agora a gente vê primeiro o endereço da primeira vez que declaramos a variável <code>hello</code> na função main. Vou repetir caso tenha ficado confuso. Se eu der <code>printf</code> direto na variável <code>hello</code> iria imprimir o valor “Hello World”. Mas como colocamos esse &amp; comercial na frente da variável, na realidade ele vai acessar o endereço onde o Hello World está gravado na memória. Tudo tem um endereço e em C, diferente de outras linguagens, temos acesso direto a esse endereço com essa notação.</p>
<p>Eu já falei do endereço de cada instrução do executável e na mesma memória estão valores que vamos criando, como essa String. Então executar um programa é executar a instrução que tá em algum endereço. E os valores que cada instrução vai usar, por sua vez, também estão em algum endereço na memória. E com esse &amp; comercial na frente da variável podemos fuçar o endereço. Pense no Hello World como o conteúdo de um arquivo texto e esse endereço como sendo uma URL de uma página web tipo <a href="http://hello.com"target="_blank" rel="noopener">http://hello.com</a>.</p>
<p>O segundo numerozão que aparece é o endereço do argumento de dentro da função <code>f1</code>, quando passamos o <code>hello</code> pra ela. Veja que o endereço é diferente, apesar do valor ser o mesmo. Ou seja, ao passar a variável pra função ela foi duplicada. E a terceira linha é a mesma coisa, outro endereço diferente que foi impresso de dentro da função <code>f2</code>. Porque quando a <code>f1</code> passou sua variável pra <code>f2</code> como argumento, essa variável foi duplicada de novo.</p>
<p>No final criamos 3 cópias desse String na memória ao longo do caminho, cada uma com endereços diferentes. Você pode ver que toda vez que damos <code>printf</code> passando a referência da variável ela imprime um número diferente. Toda vez que se passa uma variável de tipo primitivo como argumento de uma função, ela é duplicada. E isso não é só em C, mas na maioria das linguagens. Isso tem um nome, se chama “passagem por valor”, que é diferente de “passagem por referência”, que eu vou mostrar já já. A vantagem é que se a gente modificar o valor do argumento dentro da função, por exemplo a <code>f1</code>, o valor original antes de ser duplicada, por exemplo, na função <code>main</code>, não vai mudar depois que a <code>f1</code> der return.</p>
<p>Outro detalhe, o endereço é impresso negativo na tela porque o binário desse endereço começa com 1 e a função <code>printf</code> interpreta como sendo um integer com sinal, que como eu expliquei agora pouco, se começa com 1 ele acha que é um número negativo. Mas também por começar com 1 sabemos que se trata de um endereço na metade mais alta dos endereços da memória. Considere que o endereço mais baixo é o binário zero zero zero etc sessenta e quatro vezes e vai incrementando de um a um até o endereço um um um um sessenta e quatro vezes que é o endereço mais alto.</p>
<p>O espaço de memória que um programa enxerga é segmentado segundo alguma convenção. Pra facilitar a vida, num Windows da vida, ele começa a alocar memória pra drivers e coisas do sistema no final da metade alta, ou seja, começa no endereço binário um um um etc ou hex FFFF etc e vai diminuindo. Já programas de usuário, tipo seu navegador, o Spotify e tals, começa no início da metade baixa, mais próxima dos endereços que começam com zero.</p>
<p>Um adendo. Um computador antigo de 32-bits suportava um máximo de RAM de dois elevado a 32 que é pouco mais de 4 gigabytes. Quando passamos a adotar arquiteturas 64-bits, não dobramos de memória, mas sim elevamos ao quadrado. Por isso o máximo teórico é mais de 16 exabytes. Porém, nenhum chip comercial que a Intel ou AMD fabricam de fato suportam mapear esse tanto de memória ainda. Internamente as instruções lidam com valores de 64-bits mas nenhum hardware que você conseguir comprar, nem os AMD EPIC Threadripper da vida suporta tudo isso. Eles costumam ser capados em 42 ou 48 bits de endereços em vez de 64, ou seja, pouco mais de 280 terabytes. Que é coisa pra caramba, claro, mas só pra você saber que ainda não chegamos no máximo dos 64-bits em hardware comercial.</p>
<p>De qualquer forma, isso é a memória real. Mas como expliquei nos episódio de gerenciamento de memória, um processo rodando enxerga memória virtual, que é o sistema operacional mentindo pros nossos programas como se eles tivessem acesso a toda a memória teórica de 64-bits. É o que chamamos de memória virtual. Dentro desse espaço virtual, os endereços que começam do zero e vão subindo é o que chamamos de memória dinâmica, ou &ldquo;heap&rdquo; que é pra onde vai a maioria dos dados que seu programa carrega. Quando você carrega um arquivo em memória, ou quando se conecta num servidor de API e puxa JSON, ou quando pede dados no banco de dados, tudo vai pro heap. Heap em inglês é um montão, um amontoado.</p>
<p>Repetindo, lá no topo, nos endereços que começam com bit 1, é onde fica o segmento de memória reservada pra kernel, que é uma área exclusiva pra endereços de coisas do sistema operacional, mas o próximo segmento é o que chamamos de pilha ou &ldquo;stack&rdquo;. Se eu não tô muito enganado, todo processo começa com pelo menos 8 megabytes de memória alta reservada pra stack e ela pode expandir decrementando endereços pra baixo. Pode consumir até uns 2 gigabytes.</p>
<p>A heap é a mesma coisa mas do outro lado, começa na memória baixa já com pelo menos 1 megabyte reservado e se precisar de mais vai incrementando endereços pra cima. Lembre-se que o stack de um nintendinho de 8-bits tinha meros 255 bytes, que é do endereço hexa $0100 até $01FF.</p>
<p>Eu explico pra que serve essa stack no episódio da introdução mais hardcore a computadores mas vamos resumir usando o exemplo do hello world que fiz agora. Eu tenho 3 funções, a main, a f1 e a f2. Cada uma delas aloca espaço pra duplicatas da string hello world, daí imprimem o endereço onde a variável foi alocada e no final sai fora e retorna pra função que chamou ela. Aliás, em C, se uma função tem como tipo de retorno um <code>void</code> que é tipo <code>nada</code> em C, então nem precisa dar return, porque não estamos retornando nenhum valor. Vamos na ordem.</p>
<p>Ao executar o binário ele pede pra kernel do sistema operacional alocar espaço pro novo processo e inicializar seja lá o que ele precisa. A kernel vai usar o gerenciador de memória e alocar o mínimo necessário pro programa rodar e depois vai dando memória real pra ele à medida que for pedindo.</p>
<p>Depois ele pula pra primeira função que é a <code>main</code>. Agora cria a variável <code>hello</code> que é um array de chars com 12 bytes de tamanho. Isso é alocado na Stack. Agora eu chamo a função do C chamado <code>printf</code>. Não vou explicar como o <code>printf</code> funciona, mas vamos aproveitar pra explicar a mecânica dessa tal stack ou pilha. Toda chamada de função começa guardando o endereço atual de execução, o que está no tal contador de programas. No nosso pseudo-exemplo de endereços seria o número da linha do nosso código onde chama a função.</p>
<p>No momento que a função <code>main</code> chama a função <code>printf</code> ele faz tipo um bookmark do contador de programa e coloca no stack. Daí podemos substituir o endereço no contador de programa pelo endereço da função que tá chamando, no caso o endereço pra <code>printf</code>. Agora, dentro da <code>printf</code> vai alocar outras variáveis, faz de conta, umas variáveis x ou y quaisquer, que vão sendo colocadas na stack.</p>
<p>Finalmente imprime na tela o que precisava e no final dá <code>return</code>. Esse return começa a desempilhar o que alocou na stack até achar o último endereço que a <code>main</code> gravou, o tal bookmark do contador de programas, e dá jump, ou seja, pula pra ela. E pular significa colocar de volta esse endereço mais um no contador de programa, daí ele pode continuar da próxima linha depois de chamar a função. É mais ou menos isso que faz um <code>return</code>. O que tinha sido colocado na stack pelo <code>printf</code> foi liberado e voltamos a executar na próxima linha.</p>
<p>De volta ao <code>main</code> a próxima linha chama a nossa função <code>f1</code>. Mesma coisa, vamos empilhar o endereço dessa posição na stack. Como eu disse antes, ao passar a variável <code>hello</code> como argumento, estamos duplicando a variável, então vai empilhar essa duplicata na stack também. Dentro de <code>f1</code> vai chamar <code>printf</code> de novo pra imprimir o endereço dessa duplicata, por isso temos valores diferentes de endereços.</p>
<p>Já sabemos como isso funciona, então vamos pular os detalhes de <code>printf</code> pra não ficar repetitivo. Pode assumir que empilhamos e desempilhamos <code>printf</code>. Em seguida o <code>f1</code> vai chamar a <code>f2</code>, e novamente o endereço dessa linha vai ser empilhado na stack, a variável hello vai ser duplicada e empilhada na stack também, e de novo vamos chamar <code>printf</code>. por fim vai ter um return.</p>
<p>Esse <code>return</code> vai desempilhando até ver o endereço de volta à função <code>f1</code> e dar jump pra ela. Agora no <code>f1</code> não tem mais nada a não ser outro <code>return</code>, então continua desempilhando até o último endereço gravado da função <code>main</code> e dá jump de volta pra ela. E finalmente na <code>main</code> também temos um return. Agora é o último de todos, então ele sai do programa inteiro, daí o sistema operacional vai fazer a limpeza da memória desse processo e tudo termina com sucesso.</p>
<p>A razão dessa explicação longa foi pra mostrar no nível mais básico o que significa chamar funções e como variáveis são gerenciadas na stack. Toda vez que você chama funções e passa argumentos pra ela, esse registro de execução vai sendo empilhado na stack, que é uma pilha. Pilha é uma estrutura de dados super importante e uma das mais fundamentais junto com Queues, que é inglês pra filas. Elas são parecidas, mas não vou falar de queues hoje. E pilhas existem dois tipos, FIFO e LIFO.</p>
<p>A pilha que estamos usando aqui é do tipo LIFO, ou seja, Last in, first out. Então o último elemento que empilhamos é o primeiro que vai ser desempilhado. Esse é mais um tema que você precisa estudar em detalhes, como é implementada e como funciona em todos os casos. É uma das lições da matéria de Estrutura de Dados e Algoritmos que qualquer faculdade ensina e mesmo se você for autodidata deveria estudar.</p>
<p>De qualquer forma, você vai se lembrar que eu falei que essa stack costuma começar com um tamanho de 8 megas e pode expandir até uns 2 gigas. Isso é bastante pra programas de qualquer tamanho. Repetindo, um jogo de Nintendinho inteiro, seja Super Mario, Zelda ou Final Fantasy conseguia se virar com uma stack de meros 255 bytes, isso é um quarto de um mísero megabyte. Estourar esse stack é sinal de má programação e acredite, não é difícil estourar a stack. E quando ela estoura, seu programa crasheia.</p>
<p>O jeito mais fácil de estourar é via recursão. Recursão é uma função ficar chamando ela mesma sem parar. Vamos dar um exemplo, podemos fazer uma função chamada <code>f3</code> como essa aqui do lado, e ela ficar chamando ela mesma. Além disso, nessa função <code>f3</code> vou ficar passando o argumento hello toda vez. Cada chamada a <code>f3</code> vai duplicar a string de hello world na stack. Daí vamos imprimir o endereço que cada uma delas tá ocupando na stack.</p>
<p>Se compilar e executar notem como os endereços seguem uma sequência decrescente um atrás do outro. Não são números aleatórios. Como eu disse, a stack começa nos endereços mais altos, depois do espaço da kernel na memória. À medida que vamos empilhando novas chamadas de f3 e duplicando a variável hello como argumento, os endereços vão diminuindo. Ela vai do fim em direção ao meio da memória.</p>
<p>E no final, boom, o programa crasheia porque acabou a stack. A função <code>f3</code> fica chamando ela mesma infinitamente até bater no limite que o sistema operacional permite. No caso do linux dá um segmentation fault. Esse erro é bem genérico mas significa que você tentou acessar memória que não existe, ou que não te pertence, ou qualquer tipo de acesso a memória que o sistema não sabe como lidar. No nosso caso, por ter acabado a memória da stack. Se pegarmos o primeiro endereço impresso na tela e o último e subtrair um pelo outro, podemos saber quanto de memória da stack foi ocupada antes do crash.</p>
<p>Como expliquei antes, esse primeiro número é negativo porque em binário começa com o primeiro bit mais significativo sendo 1, que o <code>printf</code> entendeu como integer com sinal negativo. Mas se convertermos de volta pra hexa temos esse linguição aqui, hexa F nove sete D dois zero A oito. Mesma coisa fazemos com o último endereço que foi impresso na tela e vamos ter hexa quatro um zero nove três sete zero oito. Fazendo um menos o outro temos esse numerozão, que se formos dividindo por mil e vinte e quatro, vai dar exatamente 2 gigabytes. Então podemos estabelecer que nosso stack durou até bater 2 gigabytes.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>-109240152 = 0xF97D20A8 = 11111001 01111101 00100000 10101000
1091122952 = 0x41093708 = 01000001 00001001 00110111 00001000</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Recursão é extremamente importante na programação e você precisa garantir que uma hora essa recursão vai parar antes da stack encher. Existe um caso especial de recursão que não enche a stack agressivamente desse jeito e ela simplesmente vai fazer jump de volta pra algumas instruções pra trás sem ficar enchendo a stack a cada chamada recursiva. Isso se chama Tail Recursion e vou deixar de lição de casa pra vocês estudarem caso nunca tenham ouvido falar disso. A maioria das boas linguagens suporta tail recursion e isso diminui bastante os casos de estourar a stack.</p>
<p>Mas além da recursão temos outro problema nesse exemplo besta. Toda vez que chamamos a função <code>f3</code> recursiva, passamos a string de hello world e ela é passada por valor, ou seja, é duplicada. Cada chamada recursiva que fazemos, no mínimo, tá empilhando o endereço pra onde o return vai dar jump de volta, que é um endereço de 64-bits ou 4 bytes, “mais” a duplicata de hello world que custa 12 bytes. Só isso são no mínimo 16 bytes a cada chamada.</p>
<p>Ou seja, dos 2 gigabytes de stack, talvez uns 3 quartos tão ocupados só com duplicatas de hello world, que é um grande desperdício. Uma string idealmente deveria ser imutável. Quando precisamos modificar a String ou concatenar mais texto nela, aí sim precisaríamos fazer uma duplicata. Mas se a variável vai permanecer sempre a mesma como uma constante, não precisaríamos ficar duplicando toda vez que passamos pra outra função. Imagina se em vez de hello world essa string fosse um texto enorme, uma ou mais páginas da Wikipedia, ocupando 1 mega, ou talvez 1 giga. Em duas chamadas de função duplicando a variável passando por valor ia acabar a stack. Então o que fazemos?</p>
<p>Vamos nos lembrar que eu falei que a stack é só um dos segmentos da memória total. A memória virtual total de 64-bits caberia até 16 exabytes de dados. Meros 1 giga não é nada. Eu poderia duplicar um texto de 1 gigabyte trilhões de vezes até acabar a memória toda. Mas no nosso caso o segmento da stack acaba em 2 gigabytes. Então como fazemos?</p>
<p>Agora vem a parte mais complicada. Vou tentar explicar pra vocês o temido Pointer ou apontador. Parece difícil mas o básico é bem simples, na real. Em vez de alocar espaço na stack pra guardar meus strings ou mesmo qualquer outro array gigante, vamos alocar ela no heap. Já expliquei antes que o Heap é o segmento da memória total que começa nos endereços mais baixos perto de zero e cujos endereços vão incrementando.</p>
<p>Repetindo, enquanto os endereços da stack começam do endereço mais alto e vão decrementando em direção ao começo, os da heap começam no endereço mais baixo e vão incrementando em direção ao fim. O sistema operacional vai garantir que um não cruze os limites do outro no meio. Tecnicamente os endereços da heap podem ir incrementando até o limite de não ultrapassar os endereços do stack, entenderam?</p>
<p>Quando você instancia um objeto numa linguagem como Python, Javascript, Ruby, Java, C#, o bloco de memória que contém todos os dados do objeto ficam no heap. E na stack fica só endereço, ou a referência que aponta pra esse objeto, que numa arquitetura de 64-bits vai ter sempre o tamanho fixo de 4 bytes, independente de quanta memória foi alocada pro objeto no heap. O truque é simples, vamos voltar ao hello world e em vez de alocar na stack, vamos mover pro heap.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;

void main() {

    char hello[] = &#34;Hello World&#34;;
    printf(&#34;from main: %d\n&#34;, &amp;hello);

    char *hello2 = malloc(sizeof(hello));
    strcpy(hello2, hello);
    printf(&#34;hello2: %x\n&#34;, hello2);

    char *hello3 = hello2 &#43; 6;
    printf(&#34;from hello2: %s\n&#34;, hello2);
    printf(&#34;from hello3: %s\n&#34;, hello3);

    return;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Em C precisamos incluir o cabeçalho <code>stdlib.h</code> que nos dá acesso à famosa função <code>malloc</code> que significa “memory allocation”. O argumento que passamos é quantos bytes queremos que ela reserve no heap. No caso, podemos usar outra função chamada <code>sizeof</code> que, como o nome diz, mede o tamanho de alguma coisa. E como parâmetro pra essa função passamos a variável hello que alocamos na stack, que é 12 bytes.</p>
<p>Poderíamos passar direto o número 12, também. Mas o que o <code>malloc</code> vai fazer é procurar um segmento contínuo de 12 bytes no heap e passar pra gente o endereço que ele reservou no heap. E essa nova variável <code>hello2</code> vai guardar esse endereço, que é um número de 4 bytes. Então esse <code>hello2</code> é um ponteiro porque seu endereço aponta aonde vai estar o string na Heap. É só isso.</p>
<p>Aqui a coisa pode ficar mais cabeluda ainda, então prestem atenção. Com esse endereço em mãos podemos usar outra função do C, o <code>strcpy</code> ou string copy, passando como argumentos esse endereço e a variável hello antiga pra copiar o hello world pra esse novo espaço na heap. Essa é a única duplicata que vamos ter a partir de agora. A parte importante é entender que na variável <code>hello2</code> nós não temos a string hello world e sim o endereço pro primeiro byte na heap onde vamos encontrar essa cópia do hello world.</p>
<p>Sendo o hello2 um ponteiro com um endereço e sabendo que cada char da string tem exatamente um byte e sabendo que ela acaba quando encontramos o caracter NULO, se fizermos outro ponteiro chamado <code>hello3</code> que é <code>hello2</code> mais 6, agora vamos ter só a palavra <code>World</code> se dermos <code>printf</code> nela. Vamos compilar e rodar pra ver. Olha como imprime na tela o Hello world do ponteiro hello2 e só world do ponteiro hello3. Mas a gente não criou ou duplicou uma nova string, estamos apontando pro mesmo espaço na memória só que 6 bytes mais pra frente.</p>
<p>Essa sintaxe ou notação de asterisco é pra indicar à linguagem que essa variável é um ponteiro e queremos acessar o valor gravado no heap, nesse exato endereço. Se não colocarmos o asterisco, vemos direto só o endereço. Se colocarmos o asterismo ele trás o valor pra onde esse endereço tá apontando. Tanto as variáveis <code>hello2</code> quanto <code>hello3</code> parecem representar um string diferente mas na verdade são só endereços ou, mais corretamente, referências, ao mesmo string, só apontando pra posições diferentes no mesmo string. Pra acessar o string em si, usamos asterisco hello2 e asterisco hello3.</p>
<p>Seja um string ou seja um array, já que string é um caso especial de array, a variável <code>hello</code> não contém &ldquo;O&rdquo; array e sim o endereço pro primeiro elemento do array. Se eu quiser pegar um elemento no meio do array, primeiro pego o endereço e somo a quantidade de bytes que leva ao elemento que eu quero. Se for um array de chars, que é uma string, onde cada char tem 1 byte; pra pegar a 6a letra, basta somar 6 bytes ao primeiro endereço.</p>
<p>Quando fazemos aquela notação que tem em toda linguagem, com brackets ou colchetes, com um número no meio, a linguagem tá traduzindo por baixo em uma soma de endereço, como eu mostro aqui no código. É o endereço inicial, mais o tamanho de cada elemento multiplicado pela posição. Vejam que estou repetindo essa operação pra tentar deixar bem claro esse funcionamento.</p>
<p>Tem uma última estrutura de dados que eu quero explicar pra vocês hoje. Como eu repeti de propósito várias vezes até agora, arrays são listas que tem sempre os mesmos tipos de elementos, ou mais corretamente, que tem elementos de mesmo tamanho. Se for um string, é um array onde cada elemento tem 1 byte, se for um array de inteiros de 64-bits, todo elemento tem exatamente 4 bytes. E eu vim repetindo a mesma coisa sobre elementos iguais por causa do seguinte: e se eu quiser uma sequência onde cada elemento tem tamanhos diferentes?</p>
<p>Pra isso, em linguagens de alto nível tipo Python existe a tupla ou tuple, ou no caso de C existe Struct. Em conceito não são a mesma coisa, mas pra hoje pense que é. Vou explicar porque. Diferente de um array onde cada elemento em sequência tem o mesmo tamanho, num struct ou tupla podemos declarar tipos diferentes que vão ser concatenados um atrás do outro no linguição de bits.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;inttypes.h&gt;

struct Person {
    char nome[10];
    uint8_t age;
    uint8_t height;
};

void main() {
    struct Person person;
    strcpy(person.nome, &#34;Fabio&#34;);
    person.age = 43;
    person.height = 172;

    printf(&#34;%x\n&#34;, &amp;person);

    return;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Por exemplo, podemos declarar uma Struct ou estrutura chamada Person, cujo primeiro elemento vai ser um array de chars de 10 bytes, um nome curtinho. O segundo elemento vai ser um int de 8 bits, ou 1 byte, chamado age, pra ser a idade dessa pessoa. E finalmente o terceiro elemento vai ser a altura ou height, que vai ser um inteiro de 8 bits também.</p>
<p>Essa estrutura define um tipo de dados novo, chamado Person, cujo tamanho total sempre vai ser de 12 bytes. Agora eu posso criar uma variável que vai ser esse tipo e o C vai preencher os valores que vamos passar na sequência correta na fita de bits. Se eu fizer <code>person.age</code> igual a 43, <code>person.height</code> igual a 172 e <code>person.name</code> igual a &ldquo;Fabio&rdquo;, a cadeia de bits vai ficar exatamente assim em hexadecimal. Lembrando que cada dois dígitos em hexa representa 1 byte, começamos com o hexa quatro seis que é a letra &ldquo;F&rdquo;, daí 0x61 que é &ldquo;a&rdquo; daí chega uma sequência de zeros porque &ldquo;Fabio&rdquo; tem menos de 10 bytes.</p>
<p><code>46 61 62 69 6f 00 00 00 00 00 2B AC</code></p>
<p>O penúltimo hexa 2B é a idade 43, e o último hexa AC é 172. A Struct meio que serve como um molde, que encaixa exatamente nessa sequência de bits e quebra os valores pra dentro dos campos que criamos. No caso de idade e altura, como são inteiros de 8 bits sem sinal, sabemos que podemos ter idades de zero até duzentos e cinquenta e cinco e alturas de no máximo duzentos e cinquenta e cinco centímetros, e tirando alguma raríssima exceção, tipo se você for mais alto que o Hulk, isso cobre qualquer pessoa do mundo.</p>
<p>Única coisa nesse trecho de código que talvez deixe você confuso é porque podemos fazer <code>person.age</code> igual 43 mas não podemos fazer <code>person.name</code> igual &ldquo;Fabio&rdquo; e em vez disso eu usei a função que copia strings de duas posições da memória, que é essa <code>strcpy</code>. Só entenda que é assim que se faz em C, mas em outras linguagens como Javascript você faz do jeito mais simples de só usar &ldquo;igual&rdquo;. Coisas desse tipo que linguagens de mais alto nível facilitam. É o que eu chamaria de ergonomia da linguagem.</p>
<p>Mas com o que eu expliquei até agora, você conhece os principais tipos numéricos primitivos como integers e floats, arrays e strings, e agora structs. Um Tuple é como se fosse um Struct anônimo, sem nomes e etiquetas, parecido com um array. Aliás, toda vez que você ver um Tuple num Python da vida, vai achar que é a mesma coisa que um array. Uma Tupla é um conjunto imutável com elementos de tipos diferentes, só isso. Um Array é um conjunto mutável de elementos do mesmo tipo. Pra extrair os elementos de uma tupla, você precisa de um molde que declara qual o tamanho de cada elemento, um molde que vai ser parecido com um Struct.</p>
<p>Estamos chegando no final, e agora com tudo que aprendemos, só falta mais um truque pra vocês começarem a entender melhor como os programas funcionam. Vocês meio que já devem ter entendido que funções nada mais são que sequências de instruções que ficam localizados em algum endereço na memória. A CPU vai executando “linha a linha” e quem dita qual linha é o tal contador de programas. Daí quando uma função chama outra, ela primeiro faz bookmark desse contador na stack, e dá JUMP pro endereço dessa outra função.</p>
<p>Repetindo, é como se cada uma dessas linhas de código ficasse num endereço na memória. Estamos simulando como se fossem as linhas do código no editor de textos. Quando na função <code>main</code> lá embaixo eu chamo a função <code>f1</code>, e o compilador do C gera o binário, ele vai substituir por algo semelhante a um <code>jump</code> ou mesmo <code>goto</code> em linguagens mais antigas, pro endereço da linha da função <code>f1</code>.  No comentário é um pseudo-código de como seria se o C tivesse uma função chamada <code>jump</code>.</p>
<p>E isso é uma das razões de porque precisamos de compiladores. Ia ser chato pra caramba ter que ficar lembrando o endereço de cada função que precisamos. E aí a gente resolve mexer na função, muda de lugar, e isso muda o endereço, daí tem que mudar todo lugar que chama esse endereço na mão. Puta trampo. Em vez disso usamos nomes que representam esses endereços, como nomes de função e nomes de variáveis, e deixamos o compilador traduzir isso em endereços no binário final.</p>
<p>Isso é outro conceito importante: nomes de coisas não precisam existir no binário. Se a gente quiser podemos dizer pro compilador manter os símbolos pra debug. Símbolos são esses nomes. Os campos nome, idade e altura não existem no binário onde estão os valores. É só um linguição de bits e só. Linguagens de mais alto nível às vezes guardam esses símbolos por padrão. São metadados ou dados que explicam os dados, e metadados ocupam espaço.</p>
<p>Mas como não são necessários pra rodar o programa e sim pra debugar depois, não é bom guardar tudo porque seria um desperdício de espaço. Se você já mexeu com coisas como XML ou JSON, eles são grandes e desperdiçam bastante espaço, primeiro porque representam tudo como strings e segundo porque levam os metadados de tudo junto com os dados. Mas isso é um detalhe que vou explorar em outro episódio. Aliás, é por isso que um Google inventou coisas como Protobufs, se você estiver interessado em mais um assunto pra estudar.</p>
<p>No caso do texto do código, nós declaramos símbolos, ou nomes pra endereços e cadeias de bits, pra facilitar a vida do programador de ler o código ou debugar depois, mas a máquina em si não se importa com nada disso. Pro computador é tudo uma cadeia de bits e só. O computador não dá a mínima pra como você chama as coisas ou o significado que dá pra elas. Um código é feito pra outros programadores entenderem. Computadores só entendem os binários sem nomes e sem significados. Entenda esse conceito.</p>
<p>A parte importante é que tudo tem endereços, não só variáveis mas funções também. Em particular, em C, podemos pegar a referência de uma função e passar como argumento pra outra função, da mesma forma como eu passei o endereço da variável <code>hello</code> pro <code>printf</code> imprimir na tela, lembra? Por exemplo, vamos reorganizar esse código de criar struct numa função nova chamada <code>createPerson</code> onde passamos como argumentos o nome, idade, altura e um último argumento que é um ponteiro pra uma função que não retorna nada, então <code>void</code> mas tem como argumento um struct de Person.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;inttypes.h&gt;

struct Person {
    char nome[10];
    uint8_t age;
    uint8_t height;
};

void createPerson(char name[],
  uint8_t age,
  uint8_t height,
  void(*function_pointer)(struct Person)) {
    struct Person person;
    strcpy(person.nome, &#34;Fabio&#34;);
    person.age = 43;
    person.height = 172;

    (*function_pointer)(person);
  }

void printPerson(struct Person person) {
    printf(&#34;Person: %s %d %d\n&#34;,
        person.nome, person.age, person.height);
}

void printPerson2(struct Person person) {
    printf(&#34;nome: %s age: %d height: %d\n&#34;,
        person.nome, person.age, person.height);
}

void main() {
    createPerson(&#34;Fabio&#34;, 43, 172, &amp;printPerson);
    createPerson(&#34;Fabio&#34;, 43, 172, &amp;printPerson2);
    return;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Dentro dela fazemos igualzinho antes, onde declaramos a struct Person, preenchemos os campos mas no final executamos a função que veio nesse endereço chamado <code>function_pointer</code>. Entenda isso. a variável <code>function_pointer</code>, assim como aquela <code>hello2</code> vai ser só uma referência, um endereço pra alguma função que tem as características que declaramos, que retorna <code>void</code> ou seja, nada, e tem uma struct Person como argumento. Ao colocar um asterisco na frente dela, é como se estivéssemos dando copy e paste do nome da função de verdade aqui e executando ela.</p>
<p>A seguir vamos declarar a função pra passar pra esse <code>createPerson</code>. Vamos chamar de <code>printPerson</code>, que retorna <code>void</code> e recebe um <code>Person</code> de argumento e dentro é só um <code>printf</code> como fizemos antes. Mas note que diferente de antes, onde precisamos declarar acima as funções que vamos usar embaixo, estamos declarando o <code>printPerson</code> abaixo da função <code>createPerson</code> porque ela ainda não sabe que é essa função que vamos usar.</p>
<p>Ela só sabe que vai receber uma referência que pode ser pra qualquer outra função. Se a <code>createPerson</code> chamasse direto a <code>printPerson</code> ela precisaria ter sido declarada antes. Mas vamos ver a seguir como eu passo a referência dessa função pra <code>createPerson</code>.</p>
<p>Na função <code>main</code> chamamos a <code>createPerson</code> passando os mesmos valores de antes, &ldquo;Fabio&rdquo;, 43 e 172 e em seguida &amp; comercial e <code>printPerson</code>. Aqui estamos pegando a referência, o endereço da função e passando como argumento. Quando compilamos e executamos vemos que a saída na tela é igual antes.</p>
<p>Esse &amp; comercial <code>printPerson</code> tá pegando o endereço onde a função tá declarada e passando como argumento pra <code>createPerson</code>. É só um integer que faz cast pra uma função. Cast é a ação de declarar que tipo esse número representa pro compilador do C saber o que fazer com ela. Mas só isso não tem graça, vamos declarar uma segunda função <code>printPerson2</code> pra ver como podemos passar uma referência diferente.</p>
<p>Lá em cima, fazemos uma cópia do <code>printPerson</code>, chamamos de <code>printPerson2</code> e vamos só mudar o jeito que o <code>printf</code> mostra os dados. De volta lá embaixo vamos chamar o <code>createPerson</code> de novo, mas passando a referência pra esse novo <code>printPerson2</code>. Depois de compilar e executar, veja que imprime essa nova versão também.</p>
<p>A função <code>createPerson</code> aceita qualquer função compatível, que tenha a mesma assinatura, ou seja, o tipo de retorno e os tipos de argumentos que ele espera. Isso é o que chamamos de assinatura de uma função. Ela só recebe a referência pra função que estiver nesse endereço. Basta essa função ter a assinatura correta, ou seja, aceitar a struct de Person e retornar void, ou nada.</p>
<p>Isso de uma função ser passada como referência pra outra função é o que você já deve ter ouvido falar como um <code>callback</code>. É uma função que vai ser chamada &ldquo;call&rdquo; de volta &ldquo;back&rdquo;. Qualquer um que tenha se interessado por programação funcional sabe que uma característica importante é ter funções que recebem outras funções como argumento. A função <code>createPerson</code> é o que em funcional o povo chama de &ldquo;High Order Function&rdquo;.</p>
<p>Quando em Javascript, Python ou Ruby você usa funções como <code>forEach</code>, <code>map</code>, <code>filter</code>, <code>reduce</code> e outros, eles recebem como argumento a referência pra outra função, normalmente uma função anônima, ou seja, que não tem nome e é declarada dentro dos parênteses de argumento. E com isso você entendeu o primeiro passo pra ter uma linguagem funcional.</p>
<p>Na teoria, sim, você poderia escrever código C do jeito “funcional”, mas ninguém faz isso porque não é prático. Mas com isso você pode imaginar como C poderia ser usado pra criar uma linguagem funcional como um Lisp da vida. É assim que a gente começa a manipular uma linguagem pra aceitar paradigmas que ela não foi projetada pra fazer. E com o que aprendemos até agora eu já posso mostrar mais um passo adiante.</p>
<p>Vamos entender o primeiro passo que leva C pra uma linguagem orientada a objetos como C++ ou Objective-C. O que eu vou mostrar a seguir não é a implementação da verdade, mas só um rascunho de como poderia ser. Quando eu penso em orientação a objetos só penso em duas coisas: um ponteiro pra uma struct e ponteiros pra funções cujo primeiro argumento, o que você aprendeu num Python ou Javascript como <code>self</code> ou <code>this</code>, sendo um ponteiro de volta pra mesma struct.</p>
<p>Vamos refatorar a <code>createPerson</code> primeiro. Até agora ela tá declarando a struct na stack e quando chama a <code>printPerson</code> tá duplicando por causa da passagem por valor. Em vez disso, vamos criar a struct no Heap e economizar espaço no stack fazendo passagem por referência. Pra isso precisamos usar <code>malloc</code> passando o tamanho da struct como parâmetro. E isso pra mim é o que significa <code>instanciar um objeto na memória</code>. Basicamente alocar espaço na heap pra uma struct.</p>
<p>No caso de C, quando criamos qualquer coisa no Heap, precisamos colocar asterisco na frente da variável que guarda essa referência pra acessar o valor, mas no caso de struct em C basta trocar essa notação de &ldquo;ponto&rdquo; pra uma seta com tracinho e sinal de maior. Se você é de C++ ou PHP já viu essa notação e eu acho que ela deriva disso em C mesmo. Pra acessar os campos de uma struct de C que foi alocada na heap, sem precisar usar parênteses e ponto, basta usar a setinha, é a mesma coisa.</p>
<p>Agora vamos tirar o argumento de ponteiro de função do final do <code>createPerson</code>. Em vez disso, vamos mover pra struct de Person. Um novo campo que é um ponteiro de função chamado <code>show</code>. Dentro da <code>createPerson</code> declaramos que o campo <code>show</code> desse person que instanciamos no heap vai apontar pra referência da função <code>printPerson</code> que a gente já tinha. Finalmente, vamos mudar o tipo de retorno da função pra devolver o ponteiro dessa struct e no final podemos dar return do person que acabamos de criar e configurar.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;inttypes.h&gt;
#define Class struct

Class Person {
    char nome[10];
    uint8_t age;
    uint8_t height;
    void(*show)(Class Person *);
};

void person_print(Class Person *self) {
    printf(&#34;nome: %s age: %d height: %d\n&#34;,
        self-&gt;nome, self-&gt;age, self-&gt;height);
}

Class Person * newPerson(char name[],
  uint8_t age,
  uint8_t height) {
    Class Person *self =
      (Class Person *) malloc(sizeof(Class Person));
    strcpy(self-&gt;nome, &#34;Fabio&#34;);
    self-&gt;age = 43;
    self-&gt;height = 172;

    self-&gt;show = &amp;person_print;
    return self;
  }

void main() {
    Class Person *person2 = 
      (Class Person *) newPerson(&#34;Fabio&#34;, 43, 172);

    person2-&gt;show(person2);

    return;
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quem já programa orientado a objetos em alguma linguagem pode estar achando isso familiar. Eu chamaria essa função <code>createPerson</code> de “construtor”. Um constructor é uma função responsável por alocar espaço pra um objeto na memória Heap e configurar os valores iniciais desse objeto, retornando o endereço, ou referência, pra esse objeto.</p>
<p>Agora vamos usar isso na função <code>main</code>. Primeiro vamos armazenar o ponteiro que a <code>createPerson</code> vai criar numa variável que é um ponteiro, um asterisco person. E passamos os valores que queremos como argumento a esse construtor. E pra imprimir esses valores na tela podemos chamar direto o método deste “objeto”. E pra quem não sabe, “método” é o nome que damos a funções associadas a um objeto. O método <code>show</code> já foi configurado no construtor pra ser um ponteiro pra <code>printPerson</code>.</p>
<p>A última refatoração que precisamos fazer é mudar a função <code>printPerson</code> pra usar essa variável <code>self</code> pra acessar os campos de nome, idade e altura. Se você é de Python, deve estar começando a se sentir em casa porque métodos de objetos de Python tem explicitamente o primeiro argumento recebendo <code>self</code>. E é o que estou simulando aqui em C.</p>
<p>Em Javascript e outras linguagens orientadas a objetos, em vez de <code>self</code> chamamos de <code>this</code> mas é mais ou menos a mesma coisa, só que o primeiro argumento é implícito, você não precisa declarar ela. Mas faça de conta que a linguagem tá botando ela lá pra você. Então, só chamando <code>person</code> setinha <code>print</code> e passando <code>person</code> como o argumento <code>self</code> vai tudo funcionar. Qual seria um próximo passo pra ficar mais parecido com uma linguagem orientada a objetos?</p>
<p>Pra quem não sabe, em C tem uma coisa chamada <code>pré-processamento</code>, que é reescrever o código antes de compilar. Se você é de Javascript pense como um “transpiler”. Por exemplo, lá no topo do arquivo podemos declarar uma cerquilha <code>#DEFINE</code> e dizer que toda vez que achar escrito <code>Class</code> pode trocar pra <code>struct</code> antes de compilar. Onde tá <code>struct Person</code> podemos substituir como <code>Class Person</code>. Vamos lá usar a função de procurar e substituir do editor e boom. Olha só como de repente parece que estamos lidando com uma linguagem orientada a objetos de verdade.</p>
<p>Se você aguentou até aqui, espero que tenha conseguido minimamente visualizar o que são os diferentes tipos de dados, as diferenças entre inteiros e floats, arrays,  tuples e structs. Como dados são alocados na stack que é a pilha e como a execução de um programa com funções que chamam outras funções vai sendo empilhado e desempilhado da stack. Como tudo tem endereços ou referências, incluindo funções.</p>
<p>Algumas linguagens expõem referências a funções, mas não permitem manipular essas referências pra apontar pra outras regiões de memória. Javascript é assim. Um Java lá atrás só tinha referência a objetos, por isso a única forma de passar funções pra outras funções era primeiro encapsular numa classe anônima, instanciar e passar a referência do objeto como argumento pra outras funções. Por baixo dos panos é assim que as coisas funcionam.</p>
<p>Pra complementar, recomendo rever os vídeos sobre gerenciamento de memória. Quando fazemos programinhas simples, que alocam poucos dados e não tem nenhuma recursão maluca, o stack é mais que suficiente. E você não precisa lidar com o heap. O problema de alocar coisas no heap é que quando termina a função que fez a alocação, o ponteiro sai do stack, mas o que foi alocado no heap continua lá ocupando espaço se você não der <code>free</code> pra liberar explicitamente. Memória na heap não se desempilha sozinha. Por isso surge a necessidade de um garbage collector ou outros truques como o ARC de Swift pra limpar o heap.</p>
<p>Só que garbage collector não faz milagres. Ele sempre vai reservar mais memória do que realmente precisa e sempre vai causar pausas na execução do programa pra fazer essa limpeza de tempos em tempos. Mesmo se você tiver memória sobrando pra desperdiçar, essas pausas pra limpeza sempre vão ser um problema. E é o que linguagens como Swift ou Rust ou C tentam evitar. Por isso são melhores pra fazer coisas como sistemas operacionais, drivers ou coisas de mais baixo nível que vão ser eficientes no uso de recursos e evitar pausas de manutenção o máximo possível. O mesmo não se pode dizer de Java, C# ou Go.</p>
<p>Linguagens interpretadas como Javascript, Python, Ruby, PHP são extensões bem pesadas em cima de C. Elas adicionam diversas funcionalidades pra facilitar a programação, mas por baixo dos panos o que faz elas funcionarem mais rápido e mais eficiente são bibliotecas escritas em C. Uma biblioteca escrita só em Python ou Javascript nunca vai ser nem tão rápida nem tão eficiente. Por isso, funções como criptografia, funções de I/O pra lidar com arquivos ou pacotes de rede, funções matemáticas e tudo mais é tudo escrito em C. Eu chamo elas de linguagens &ldquo;cola&rdquo;.</p>
<p>Você só escreve a cola em Python, que é uma sintaxe mais simples, pra mexer com funções mais complicadas que estão escritas em C ou C++. Por exemplo, em machine learning, Python não é importante porque as funções de um Tensorflow são todas escritas em C++. Se você usar SciPy pra computação científica, de novo as funções são todas em C++ ou Fortran. Nenhuma linguagem cola é rápida o suficiente pra essas coisas. São boas como cola, pra consumir essas coisas mais fácil.</p>
<p>Se você mexe em machine learning em Python, você é só um consumidor de ferramentas escritas em C++ ou C e faz cola em Python. Não tem nenhum problema nisso, e na prática a maioria de vocês assistindo nunca vai precisar de mais que isso. Linguagens que são &ldquo;cola&rdquo; nunca vão ser boas pra fazer o que eu chamo de &ldquo;sistemas&rdquo;. Coisas como drivers, criptografia, processamento massivo de dados como um banco de dados ou mesmo protocolos de rede eficientes.</p>
<p>Elas sempre vão ser só a cola pra sistemas feitos em C++ ou C. Por isso eu recomendo que você saiba no mínimo uma linguagem de mais baixo nível como C ou Rust. E junto uma linguagem cola pra ter produtividade como Javascript ou Python. Quem só sabe linguagens &ldquo;cola&rdquo; sempre vai achar que o que acontece no baixo nível é &ldquo;mágica&rdquo;.</p>
<p>E com o episódio de hoje eu espero ter conseguido quebrar um pouco dessa &ldquo;mágica&rdquo; e fazer vocês enxergarem suas linguagens favoritas com outros olhos. Claro, tudo que eu falei rápido em um episódio é o que se ensina em no mínimo um semestre em ciências da computação, talvez um ano ou até mais.</p>
<p>Eu só mencionei estruturas de dados simples, não falei de listas ligadas, árvores, red black, AVL e nem cheguei a falar dos algoritmos pra lidar com essas estruturas. Quem sabe em outro episódio no futuro, mas entenda que tudo que eu falei hoje é o básico em ciências da computação.</p>
<p>Se você se considera programador e nunca viu nada do que expliquei hoje, considere estudar e treinar mais. Isso vai te ajudar a subir pro próximo nível na programação. Se ficaram com dúvidas mande nos comentários abaixo, inclusive se já sabem de tudo que eu disse aqui, ajudem a responder às dúvidas dos colegas nos comentários também. Não deixem de assinar o canal e clicar no sininho pra não perder os próximos episódios. Compartilhem o video pra ajudar o canal. A gente se vê, até mais!</p>
<p>Links:</p>
<ul>
<li>Integer (Wikipedia) (<a href="https://en.wikipedia.org/wiki/Integer_%28computer_science%29"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Integer_(computer_science)</a>)</li>
<li>Two’s Complement (Wikipedia) (<a href="https://en.wikipedia.org/wiki/Two%27s_complement"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Two%27s_complement</a>)</li>
<li>How numbers are encoded in JavaScript (<a href="https://2ality.com/2012/04/number-encoding.html#:~:text=JavaScript%20numbers%20are%20all%20floating,binary%20format%2C%20in%2064%20bits"target="_blank" rel="noopener">https://2ality.com/2012/04/number-encoding.html#:~:text=JavaScript%20numbers%20are%20all%20floating,binary%20format%2C%20in%2064%20bits</a>.)</li>
<li>FLOATING POINT VISUALLY EXPLAINED (<a href="https://fabiensanglard.net/floating_point_visually_explained/"target="_blank" rel="noopener">https://fabiensanglard.net/floating_point_visually_explained/</a>)</li>
<li>What Every Computer Scientist Should Know About Floating-Point Arithmetic (What Every Computer Scientist Should Know About Floating-Point Arithmetic (oracle.com))</li>
<li>IEEE-754 Floating Point Converter (IEEE-754 Floating Point Converter (h-schmidt.net))</li>
<li>Number.MAX_SAFE_INTEGER (Number.MAX_SAFE_INTEGER - JavaScript | MDN (mozilla.org))</li>
<li>Signed Binary/Decimal Conversion Using the Two&rsquo;s Complement Representation (Signed Binary/Decimal Conversion (ubc.ca))</li>
<li>C - Pointer arithmetic (C - Pointer arithmetic - Tutorialspoint)</li>
<li>Why Discord is switching from Go to Rust (<a href="https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f"target="_blank" rel="noopener">https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f</a>)</li>
</ul>
]]></content:encoded><category>linguagens-de-programacao</category><category>ciencia-da-computacao</category><category>programacao</category><category>akitando</category></item><item><title>[Akitando] #91 - Qual a REAL diferença entre Arquivos Binário e Texto?? 🤔</title><link>https://akitaonrails.github.io/2021/01/20/akitando-91-qual-a-real-diferenca-entre-arquivos-binario-e-texto/</link><guid isPermaLink="true">https://akitaonrails.github.io/2021/01/20/akitando-91-qual-a-real-diferenca-entre-arquivos-binario-e-texto/</guid><pubDate>Wed, 20 Jan 2021 10:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/oSCVb4Ts-G4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Um arquivo texto É um binário! 😲&lt;/p&gt;
&lt;p&gt;E como um sistema operacional diferencia entre os dois? Porque ele sabe executar um binário compilado de C, sabe executar um script de Python, mas também sabe dar erro se tentamos executar um texto qualquer?&lt;/p&gt;
&lt;p&gt;Como isso funciona de verdade por trás dos panos?&lt;/p&gt;
&lt;p&gt;Hoje é dia de escovar bit, e entendermos o que é ASCII, o que é ELF e muito mais!&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/oSCVb4Ts-G4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um arquivo texto É um binário! 😲</p>
<p>E como um sistema operacional diferencia entre os dois? Porque ele sabe executar um binário compilado de C, sabe executar um script de Python, mas também sabe dar erro se tentamos executar um texto qualquer?</p>
<p>Como isso funciona de verdade por trás dos panos?</p>
<p>Hoje é dia de escovar bit, e entendermos o que é ASCII, o que é ELF e muito mais!</p>
<p>ERRATA:</p>
<ul>
<li>em 05:29 eu falo &ldquo;porque 10 em decimal é A&rdquo; mas eu quis dizer &ldquo;10 em HEXAdecimal é A&rdquo;</li>
<li>em 13:53 eu falo que depois de 69 é 70, mas tá errado, em hexa depois de 0x69 é 0x6A, depois 0x6B e assim por diante até 0x6F e só depois seria 0x70</li>
</ul>
<p>ÍNDICE:</p>
<ul>
<li>00:00 - Intro</li>
<li>01:14 - Hello World</li>
<li>04:30 - arquivos texto</li>
<li>04:59 - arquivos binários</li>
<li>06:13 - como um computador boota?</li>
<li>08:44 - fontes num Nintendinho</li>
<li>13:29 - ASCII</li>
<li>17:24 - segurança: homografia</li>
<li>19:21 - binários executáveis (ELF)</li>
<li>25:44 - binários JPEG</li>
<li>27:20 - Shells (REPL)</li>
<li>30:10 - Encerramento</li>
</ul>
<p>LINKS:</p>
<ul>
<li>Techquickie: The Magic Behind RAM (<a href="https://www.youtube.com/watch?v=UKTc9toqgU0"target="_blank" rel="noopener">https://www.youtube.com/watch?v=UKTc9toqgU0</a>)</li>
<li>Cyrillic (Unicode block) (<a href="https://en.wikipedia.org/wiki/Cyrillic_%28Unicode_block%29"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)</a>)</li>
<li>IDN homograph attack (<a href="https://en.wikipedia.org/wiki/IDN_homograph_attack"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/IDN_homograph_attack</a>)</li>
<li>Memory Prices 1957+ (<a href="https://jcmit.net/memoryprice.htm"target="_blank" rel="noopener">https://jcmit.net/memoryprice.htm</a>)</li>
<li>Introduction to the ELF Format Part II : Understanding Program Headers (Introduction to the ELF Format Part II : Understanding Program Headers (k3170makan.com))</li>
<li>Characters, Symbols and the Unicode Miracle - Computerphile (<a href="https://www.youtube.com/watch?v=MijmeoH9LT4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=MijmeoH9LT4</a>)</li>
<li>The 8-Bit Guy - Commodore History (<a href="https://www.youtube.com/watch?v=eP9y_7it3ZM&amp;list=PLfABUWdDse7Y6LLPlfsHKcvBCgqaudzVY"target="_blank" rel="noopener">https://www.youtube.com/watch?v=eP9y_7it3ZM&list=PLfABUWdDse7Y6LLPlfsHKcvBCgqaudzVY</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Dois videos atrás comecei a falar sobre segurança, mas pra continuar eu preciso começar a complementar o assunto de  introdução a computadores com um pouco mais de escovação de bits. Se você assistiu todos os meus vídeos pelo menos já sabe um pouco de como um computador funciona. Como você pode carregar um programa via uma ROM por exemplo, como ele vai instrução a instrução, preenche registradores com resultados, depois você pode endereçar coisas na RAM e assim por diante.</p>
<p>Nos episódios que falei da máquina universal de Turing e a arquitetura de Von Neumann, você já sabe que uma característica importante é o fato de programas e dados compartilharem o mesmo espaço numa fita e como o computador vai lendo um bit de cada vez dessa fita, seja lendo ou escrevendo nela. Todo iniciante sabe que computadores lidam com bits e bytes. Mesmo assim muitos tem dificuldade de entender o que de fato isso significa. Hoje vou falar um pouco sobre formatos de arquivos, mais especificamente sobre qual a relação de arquivos binários ou textos planos, e no final espero que vocês tenham uma noção melhor de como as coisas funcionam por baixo dos panos. Hoje vamos literalmente escovar bits.</p>
<p>(&hellip;)</p>
<p>Vamos começar com uma coisa muito simples, o famoso Hello World, mas escrito em C, que é esse codigozinho idiota aqui do lado. Em C a gente declara dependência ao arquivo stdio.h ou standard i/o ponto header, porque é um arquivo de cabeçalho. Todo programa em C inicia nessa função <code>main</code> e dentro temos a chamada da função <code>printf</code> que só existe porque incluímos o stdio.h antes. Passamos a string <code>Hello World</code> e finalmente retornamos 0 que indica pro sistema operacional que o programa terminou com sucesso. E é só isso, depois desse ponto o programa termina e sai.</p>
<p>Eu escrevi esse código usando um editor de textos, no caso o Vim no meu Linux. E salvei num arquivo chamado hello.c. Se você for total iniciante poderia pensar &ldquo;ah, isso é um programa, portanto posso tentar rodar esse programa né?&rdquo; É uma pergunta válida. E se tentarmos &ldquo;rodar&rdquo; esse arquivo? Você começou a estudar um pouco de Linux, pra executar alguma coisa que tá no mesmo diretório pode simplesmente escrever ponto barra pra dizer que não é pra procurar no PATH e sim no diretório atual, daí hello ponto c e &hellip; permission denied, fuck.</p>
<p>Mas você sabe como resolver! Google! E como mostra no primeiro link, claro, precisa dar permissão pra execução nesse arquivo. No Windows basta que alguma coisa termine em ponto exe ou ponto bat que executa, mas no Linux precisa fazer <code>chmod +x hello.c</code> e agora dá pra tentar de novo e &hellip; what the fuck, vários erros. “Essa porra de C é difícil mesmo, melhor voltar pro CSS” (ba dum).</p>
<p>Não, tô zoando. Esses erros não são de C. Quando você tenta executar um arquivo de texto na realidade ele tá fazendo o shell executar. Isso é o mesmo que executar <code>bash</code> e passando <code>hello.c</code> como parâmetro. Bash ou Zshell ou Fish ou Powershell são interpretadores, que recebem comandos direto que você digita direto na linha de comando ou via um script, que é o que o bash tá achando que é esse hello ponto c. Os erros que estamos vendo não é de C e sim de Bash.</p>
<p>Já já volto pra interpretadores, mas falando sério, eu espero que a esta altura você saiba que C primeiro precisa ser compilado, que em resumo é transformar esses comandos que eu escrevi num binário compatível com o processador por baixo. E pra isso podemos usar clang ou gcc, mas pra ser old school vou usar gcc. Um simples <code>gcc hello.c -o hello</code>. E pronto, agora posso executar ponto barra hello e boom, Hello World, que era o que eu queria.</p>
<p>Se você já é programador isso deveria ser trivial, mas vamos pensar como iniciantes. Porque isso que eu fiz funciona? No Linux temos comandos como <code>cat</code> que lêem o conteúdo de um arquivo e imprime na tela. Se eu der <code>cat hello.c</code> aparece o que eu escrevi. Mas se eu der <code>cat hello</code> vem esse monte de lixo. O que é esse monte de lixo? Pra entender isso temos que dar um passo pra trás.</p>
<p>Relembrem o computador de Turing. Ele entende uma fita de bits. Toda memória, seja RAM ou ROM, todo SSD ou HD ou pen drive ou, no nosso caso, arquivo texto ou arquivo binário é só isso: uma fita de bits. O comando <code>cat</code> que usamos na verdade faz mais do que só ler um arquivo: ele traduz o conteúdo de um arquivo. E ele é burrinho, a única coisa que ele faz é tentar converter uma fita de bits em uma representação de texto.</p>
<p>Uma coisa que hoje é meio padrão é que representamos texto humanamente legível usando uma tabelona chamada UNICODE. Até pouco tempo atrás, antes de UNICODE se disseminar, usávamos uma tabela menor chamada ASCII. Unicode é um assunto super cabeludo que tem vários posts e videos já explicando por isso não vou detalhar demais, entenda que é complexo. Se você acha que entende tudo de Unicode, você não entende Unicode.</p>
<p>Em vez de usar o comando <code>cat</code> vamos usar o comando <code>xxd</code> que se não me engano já mostrei no video de aprendendo sobre computadores com super mario. Se fizermos <code>xxd -b hello.c</code> vamos ver a fita de bits organizado em colunas de 8 bits que é um byte, 6 bytes de cada vez pra caber na tela. Na esquerda temos a posição desses bytes, contado em hexadecimal, não confundir com decimal. Podemos contar assim: começa na posição zero, daí um, dois, três, quatro, cinco, daí pula pra próxima linha começando na posição seis, continua, sete, oito, nove, “A”, porque dez em hexadecimal é A, daí onze é B e pula de linha e continua na linha seguinte na posição C e assim por diante.</p>
<p>Na coluna da direita temos a tradução desses bytes usando a tabela ASCII. Então vemos o segundo byte por exemplo, zero um um zero um zero zero um que é a letra &ldquo;i&rdquo;. Note que temos outro &ldquo;i&rdquo; na palavra <code>stdio</code> e esse &ldquo;i&rdquo; está no segundo byte da linha que começa no endereço &ldquo;C&rdquo; que falei antes. Viu? Olha como o primeiro &ldquo;i&rdquo; e o segundo &ldquo;i&rdquo; é a mesma sequência zero um um zero um zero zero um. A gente traduz os bytes em letras que conseguimos ler, mas o computador só vê isso, uma fita de bits. Por convenção quebramos a fita em blocos de 8 bits, que chamamos de bytes. E 1 byte, na tabela ASCII, traduz pra uma letra.</p>
<p>O que é uma tabela ASCII? Todo computador hoje vem equipado com essa tabela pré-instalada. Entenda assim: uma CPU sozinha não faz nada. Você liga a energia e ela fica só lá sentada esperando. Relembre o video do Guia mais hardcore de introdução à computação. Nele eu mostro como gravamos um programinha num chip de EPROM e a CPU sempre vai procurar alguma coisa num certo endereço de memória pra começar a executar. De forma simplificada, num computador moderno esse chip de EPROM é o que chamamos de “firmware” ou BIOS. Por iss,o se você já atualizou o firmware sempre vem avisos de não interromper o processo no meio. Porque se falhar a atualização do firmware, o computador vai ficar sem o programa que inicia tudo.</p>
<p>BIOS é acrônimo pra sistema básico de entrada e saída. E firmware é uma classe específica de programas que oferece funções que chamamos de baixo nível. Por exemplo, seu Windows ou Linux tá instalado num HD, mas pra saber que o HD existe e como ler bits do HD e depois identificar que tem pentes de RAM e carregar o Linux na RAM, você precisa de um programa antes. Esse programa é o firmware, que oferece funções básicas pra se comunicar com dispositivos de entrada e saída, como a memória.</p>
<p>Se você assiste canais de montagem de PCs como o Linus Tech Tips, vai ver eles sempre se preocupando num tal de POST depois que o computador liga. POST é acrônimo pra Power On Self Test ou auto-teste depois de ligar. É a BIOS rodando e identificando corretamente todo o hardware. Depois desse teste é que ele começa a sequência de boot do sistema operacional, procurando numa ordem específica tipo pendrive antes do HD por exemplo. E se for HD procurando numa posição específica do HD. No caso do formato antigo MBR ou Master Boot Record, são os primeiros 512 bytes da primeira partição do HD.</p>
<p>Enfim, sem dar muita tangente, uma das coisas que a BIOS carrega é uma tabela de conversão de bits pra letras que um ser humano consegue ler. Essa é a tabela ASCII, por isso logo que você liga o computador, ele tem a habilidade de escrever textos na tela pra mostrar o boot antes mesmo do sistema operacional iniciar. É o que ele usa pra montar a tela de configuração da BIOS quando você aperta F12 por exemplo. Se não ficou claro a importância disso, lembram do episódio de video game? Um videogame bem antigo, tipo nintendinho de 8-bits da vida? Então, essa é uma época onde chips de memória eram bem caros. Um chip de 64 kilobytes podia ser tão caro quanto hoje é um pente de 64 GIGABYTES. Por isso você não podia desperdiçar um byte.</p>
<p>Uma das formas de economizar memória é que a BIOS de um videogame antigo é tão simples que ele sequer sabe converter bytes em letras. Aliás, um console de 8-bits eu acho que nem tinha equivalente de BIOS, bootava direto do cartucho. Já notou que todo jogo antigo tem fontes diferentes? Não é porque o sistema vem com várias fontes, que nem hoje você tem Arial ou Verdana ou Times pra escolher. As tais &ldquo;fontes&rdquo; precisavam vir na ROM do cartucho, de cada jogo. E pra economizar espaço eles só colocavam as letras e símbolos que de fato o jogo fosse usar. Se tivesse alguma letra não precisava, ele pulava. Vamos dar um exemplo, vamos abrir a ROM de um jogo antigo como o Super Mario original com o emulador Mesen.</p>
<p>Só um parêntese aqui, o Mesen é um excelente emulador se você quiser escovar bit de binário de cartuchos porque ele tem várias ferramentas de debugging, você pode escrever assembly de 6502 e ele vai converter pro binário e inserir onde você quiser, pode colocar pontos de parada, vasculhar valores nos registradores e tudo mais. Vale a pena pra quem quiser fuçar mais a fundo. Mas pra agora vamos usar uma ferramenta que mostra o conteúdo de segmentos de sprites do cartucho de forma visual.</p>
<p>Vamos direto pra região de memória do cartucho que, por convenção, se chama CHR ou characters. Olhem aqui as letras desenhadas do jogo, literalmente caracteres. Como eu expliquei no outro video um Nintendo de 8-bits lida com sprites em vez de pixels individuais, que são conjuntos de oito por oito pixels. Por acaso o caracter&quot;1&quot; está na posição 1, a &ldquo;2&rdquo; está na posição 2 e assim por diante. Pense nessa região de memória como hoje seriam as fontes True Type ou Open Type que você de front-end e CSS tá cansado de ver. Fontes modernas carregam em alguma região específica da memória e ocupam espaço.</p>
<p>O problema é que pra representar as letras de oito por oito pixels em memória, só de zero a nove e de A a Z são oito vezes oito vezes trinta e seis letras, o que dá dois mil e trezentos bytes ou mais de 2 kilobytes. A ROM do Super Mario inteiro ocupa 40 kilobytes, então 5% do cartucho já tá ocupado só com essas letras. Por isso evita-se fazer letras maiores, opções de maiúscula e minúscula, porque só de fazer isso ia quase dobrar o espaço usado.</p>
<p>Pra entender a importância de economizar espaço, por volta de 1982, 1 megabyte custava mais de 4 mil dólares. Só esses 2 kilobytes representavam pelo menos 7 a 10 dólares! Consegue imaginar isso? Uma moeda de um quarter ou 25 centavos pra cada letra que eu acabei de mostrar. Em 1990, quase 10 anos depois, o mesmo megabyte caiu de 4 mil dólares pra menos de 100 dólares. E é por isso que só a partir dos anos 90 começa a ficar viável fazer coisas que desperdiçam memória massivamente como interfaces gráficas e fontes variadas pré-instaladas. Nos dias de hoje, 1 megabyte é super insignificante, é só uma fração de centavo, de Real.</p>
<p>Quando os microcomputadores começaram a se popularizar, uma coisa que era meio necessário era ter um mínimo de fontes pré-instaladas pra podermos digitar alguma coisa e pro computador conseguir nos mostrar mensagens na tela. E com o tempo passamos a adotar a convenção da tabela ASCII, pra ser possível transferir um arquivo texto de um computador pra outro de outro fabricante. Se cada fabricante usasse tabelas diferentes, o arquivo texto só ia conseguir ser lido num outro computador do mesmo fabricante.</p>
<p>No caso dos videogames com cartucho como Super Mario que eu mostrei, o código 1 representa o símbolo gráfico 1. Pra escrever meu nome, AKITA, eu representaria na memória como os códigos hexadecimais 0A, 14, 12, 0D e 0A de novo. Isso ocupa 5 bytes, porque cada número em hexadecimal representa 4 bits, dois números são 8 bits, portanto 1 byte. Meu sobrenome tem 5 letras, portanto 5 bytes. Em um byte é possível ir de 00 até FF que seria 256 opções. Mas no nosso caso a tabela termina no código 24. Depois da última letra, do código 25 pra cima ele mapeia pra outros sprites que são os desenhos tipo dos blocos que o Mario quebra, dos sprites que formam personagens como o próprio Mario ou os Goombas.</p>
<p>Pra ter uma imagem na cabeça, o que hoje eu chamaria de tabela de fontes, de uma Arial ou Times, no cartucho de um nintendo é essa área de memória da ROM chamada CHR e as fontes são todos os sprites do cartucho. É como se fizéssemos um jogo só com as letras que aparece no seu teclado, que o povo fazia mesmo, em DOS antigo. Mas num console ou microcomputadores dos anos 80 como os Vic-20, ZX Spectrum, a gente substituia os caracteres de texto pra caracteres “gráficos”. Por isso em teclados de VIC-20 ou Commodore 64 você até já tinha uma tabela alternativa de caracteres, e com eles você podia desenhar na tela sem precisar especificar pixel a pixel toda hora. Pode ser meio complicado de visualizar se você nunca viu isso, então recomendo assistir o canal do 8-Bit Guy, em particular sobre os computadores da Commodore.</p>
<p>Voltando ao nosso exemplo do hello.c, vamos abrir o binário do arquivo texto em formato hexadecimal. É a mesma coisa. Note que o segundo caracter é a letra &ldquo;i&rdquo; que a gente viu em binário agora pouco, em hexadecimal é o código 69. Daí na mesma linha temos o &ldquo;i&rdquo; da palavra &ldquo;stdio&rdquo; que é a terceira letra da direita pra esquerda e, claro, temos o código 69. Na tabela que temos pra representar as letras, o código 69 é &ldquo;i&rdquo;, então o código 70 vai ser &ldquo;j&rdquo;, 71 vai ser &ldquo;k&rdquo; e assim por diante até Z. Por acaso todo PC usa a mesma tabela e o nome dessa tabela é a tal ASCII.</p>
<p>ASCII é abreviação pra American Standard Code Information Interchange. Essa conversão se iniciou na época dos telégrafos, então é lá pelo meio do século XX. E foi estabelecido um padrão de 7-bits pra trocar textos. 7 bits é suficiente pra armazenar 2 elevado a 7 ou seja 128 caracteres. Contando 10 números, mais 26 letras do alfabeto em maiúscula, mais 26 letras em minúscula, já ocupamos metade da tabela. O resto é ocupado por símbolos como os aritméticos de adição ou subtração, parênteses, colchetes e assim por diante. 7-bits é suficiente pra maior parte dos textos escritos em inglês e sobra algum espaço ainda pra alguns caracteres acentuados.</p>
<p>Claro, ASCII, sendo um padrão americano, funciona bem pra qualquer língua próxima do inglês. Português usa uma variação que encaixa coisas como cedilha. Espanhol precisa no “N” com til e interrogação ao contrário. Outros países adotaram tabelas alternativas. Imagina no Japão por exemplo. 128 caracteres não cabe nem uma fração de todos os símbolos que se precisa como os famigerados Kanjis. Pra comportar mais caracteres precisamos de mais bytes e daí nasceram extensões como o padrão EUC que é Extended Unix Code e pro Japão em particular tem o EUC-JP. Pra China tem EUC-CN, pra Coréia tem EUC-KR e assim por diante.</p>
<p>Tecnicamente um EUC-JP é mais compatível com ASCII porque em códigos que começam com 0, ou seja de 7-bits, ele corresponde aos mesmos códigos da tabela ASCII e por isso foi usado em coisas como UNIX versão internacional. No Japão surgiram mais de uma tabela. Apesar da EUC-JP ser mais simples pra usar em conjunto com caracteres ASCII não foi a mais usada. A que eu mais vejo é a Shift-JIS que foi criada, ironicamente, pela ASCII Corporation do Japão e pela Microsoft. Quando se usa caracteres de 2 bytes, o Shift-JIS é uma dor de cabeça quando mistura caracteres ASCII porque tem ambiguidade do mesmo código ser usado pra símbolos diferentes dependendo de como foi codificado.</p>
<p>O importante é entender que línguas como chinês, coreano ou japonês são formados por ideogramas, e não sílabas com consoantes e vogais. Então precisa haver uma forma de mapear milhares de símbolos. Com um único byte podemos mapear não mais que 256 caracteres. Pra manter 1 byte precisam ser usados tabelas diferentes pra mapear o mesmo número pra símbolos diferentes. Você imagina a dor de cabeça de ficar trocando entre tabelas diferentes. Quando usamos 2 bytes, aí as possibilidades aumentam pra 2 elevado a 16, que dá mais de 65 mil símbolos possíveis e aí já cabe quase tudo que precisa. Em Unicode temos UTF-8 pra linguagens ocidentais que se aproxima da tabela ASCII e UTF-16 ou UTF-32 pra outras línguas por exemplo. Quanto mais bytes se usa por caracter, mais espaço é desperdiçado num arquivo de texto.</p>
<p>Agradeça por ser programador num país ocidental que pode usar só ASCII ou UTF-8, porque coisas simples como gravar um arquivo texto é simples. Em chinês ou japonês, especialmente se tiver que lidar com tabelas antigas como EUC-CN em vez de só Unicode, você ia chorar lágrimas de sangue. Imagine fazer coisas como algoritmos de procura de palavras, algoritmos de ordenação de palavras, algoritmos de encontrar palavras similares. Coisas que mesmo em inglês ainda não é perfeito como transcrever um áudio em palavras. É muito mais complicado.</p>
<p>Aproveitando essa tangente vale falar de problemas de segurança que apareciam quando você entende que tá num mundo que representa letras diferente do padrão ASCII. Os símbolos em pixels que desenha na tela, que num nintendinho chamamos de sprites, no mundo de fontes chamamos de glifos. Agora, a letra maiúscula &ldquo;A&rdquo; em latim, ou a mesma letra em cirílico, ou a mesma letra em grego, todas têm a mesma imagem ou glifo de &ldquo;A&rdquo; nosso, mas cada um é um código diferente nas tabelas Unicode. No caso são respectivamente os códigos 41, 410 e 391. Porque isso é importante? Porque eu poderia registrar um domínio que já existe, digamos example ponto com e trocar o código UTF-8 do &ldquo;a&rdquo; minúsculo ASCII pro “a” minúsculo em cirílico. Humanos vão enxergar escrito &ldquo;example&rdquo; mas são duas palavras diferentes, com códigos diferentes.</p>
<p>Não entendeu? Em binário, via UTF-8 ou ASCII, é assim que a palavra <code>example</code> é representado: <code>65 78 61 6d 70 6c 65 0a</code> mas e se trocarmos pelo &ldquo;а&rdquo; em cirílico, que é o alfabeto russo? Ele fica diferente e 1 byte mais longo, assim <code>65 78 d0 b0 6d 70 6c 65 0a</code> esse <code>d0b0</code> é a letra &ldquo;a&rdquo; em cirílico. Se abrirmos com o <code>xxd</code>, que tenta representar segundo a tabela ASCII, ele não sabe que letra é, então bota pontos no lugar. São dois bytes que não representam nada na tabela ASCII e isso quebra a palavra pra gente.</p>
<p>Mas num navegador que entende Unicode, você abre no gmail um e-mail com um link que claramente diz facebook ponto com. Mas o &ldquo;a&rdquo; não é o código 61 e sim o 430 em cirílico. Mas você não tem como saber a diferença só olhando pro glifo. Esse tipo de ataque era possível antigamente mas hoje em dia não mais porque os registras filtram quando você tenta registrar. Era um tipo de spoofing via registro de domínios fakes. É semelhante a um typosquatting que é quando você registra domínios que soam parecido com o original mas com um errinho que pode passar batido se você lê rápido, ou tá acostumado a falar que nem uma criança, por exemplo <code>santader.com</code> em vez de <code>santaNder.com</code>.</p>
<p>Voltando ao nosso exemplo original do hello world. Aprendemos que nosso arquivo <code>hello.c</code> é o que chamamos de um arquivo plain text, ou texto plano, que é um arquivo que contém apenas binários que mapeiam pra algum caracter numa tabela como ASCII. Mas pra todos os efeitos práticos, um arquivo texto também é um arquivo binário. A extensão não modifica nada no arquivo, é só uma convenção. Dentro dele tem uma fita, uma cadeia de bytes onde cada byte, de acordo com a tabela UTF-8 ou ASCII mapeia cada byte pra um glifo, um desenho de uma letra, número ou símbolo qualquer.</p>
<p>Eu estou sendo bem demorado nessa explicação porque vocês precisam entender desde cedo que o que você enxerga como “texto” é apenas uma representação do shell ou do editor de textos, o arquivo que ele abre é um binário como qualquer coisa no sistema. Tudo é binário. O que popularmente chamamos de “binário” é um tipo específico de binário, os executáveis. E agora vou mostrar qual a diferença do arquivo <code>hello.c</code> que é um binário pra texto do binário executável <code>hello</code> que foi gerado pelo <code>gcc</code> compilando o código que escrevemos dentro desse <code>hello.c</code>.</p>
<p>Se usarmos o mesmo comando <code>xxd hello</code> vamos ver um monte de sujeira na tela. Diferente do arquivo texto que nós digitamos, aqui tem só umas letras e símbolos jogados. Mas note que lá no começo, o arquivo começa com um código hexadecimal 7f e depois escreve ELF. Isso indica que este é um binário formato ELF que significa Executable and Linking Format, um formato padrão de executável que data da era dos UNIX originais.</p>
<p>Quando tentamos executar esse binário o sistema operacional, no meu caso o Arch Linux, vai identificar esses primeiros bytes e começar a checar se é um executável ELF válido. Pra isso vai validar o cabeçalho do arquivo, que ocupa um espaço específico de bytes no começo do arquivo. Tem um endereço especial pra identificação de cada coisa. Por exemplo, no 5o byte se tiver o valor 1 significa que é um binário de 32-bits, se for 2 significa que é 64-bits e de fato, temos 2, portanto é 64-bits.</p>
<p>Se você nunca entendeu o que é um protocolo binário, esse cabeçalho é um tipo de protocolo. Ele delimita campos dentro de uma cadeia de bytes. Por exemplo, do byte 0 até o byte 4 precisa ter a palavra <code>ELF</code> em ASCII, no byte 5 temos identificação de 32 ou 64-bits, e assim por diante. Cada campo tem um endereço de começo, um endereço de fim e alguns valores específicos e arbitrários que se espera encontrar nesse espaço, segundo a definição do protocolo ELF.</p>
<p>É tudo só um linguição de bytes, mas se satisfazer esse protocolo, essas regras de posição e valores, vai ser um executável ELF de Linux por exemplo. Um protocolo de rede é a mesma coisa, um linguição de bytes que tem certos campos em certas posições. Uma hora ainda vou falar sobre redes, mas isso deve dar uma noção pra agora. Lembre-se, é um computador de Turing, programa e dados estão todos misturados na mesma fita de bytes.</p>
<p>Continuando, tem um tanto de lugares vazios pra poder adicionar informações extras se precisar numa futura nova versão de ELF, mas o programa de verdade só começa a partir do endereço hexadecimal 0x1000. O que tinha antes eram meta-dados, ou dados que descrevem os dados do binários. E é isso que queremos dizer com dados e programas serem a mesma coisa. É tudo uma fita de dados. Aqui você vê que fica mais denso de bytes diferentes.</p>
<p>Mas note o primeiro, hexadecimal f3 depois 0f daí 1e e fa. é um comando em assembly de Intel x86 64-bits, o comando <code>endbr64</code>. Assim como tínhamos uma tabela de caracters como ASCII, pra saber sobre esse assembly, você precisa saber outra tabela, a que mapeia hexadecimais com comandos de assembly. Cada arquitetura de processadores tem uma diferente. Mesmo no mundo Intel, temos instruções diferentes entre 32-bits e 64-bits, a maioria é compatível mas em 64-bits obviamente temos mais funcionalidades diferentes. Tudo em computadores é sempre mapear bytes pra alguma coisa. Por isso quanto antes você se acostumar a conviver com hexadecimais, melhor.</p>
<p>Diferente de um arquivo texto onde os bytes são traduzidos da tabela UTF-8 pra imprimir os glifos na tela pra gente poder ler, quando o Linux detecta que é um executável ELF, ele valida os metadados do cabeçalho e a partir do endereço 1000 começa a mandar as instruções pro processador. Pra ficar menos complicado de enxergar, em vez do comando <code>xxd</code> podemos usar outra ferramenta que vai traduzir o hexadecimal pros mneumônicos de assembly, no caso o <code>objdump</code>.</p>
<p>Ele já pula o cabeçalho todo e vai direto pro endereço 1000 e veja os mesmos f3 0f 1e fa que é comando endbr64 que falei. Daí começa uma sequência de iniciação que vai executar essas instruções <code>sub</code> de substrair, <code>mov</code> de mover e assim por diante. Mas e um executável de Windows? Se a gente copiar o executável do Mesen que eu tava usando agora pra dentro do Linux podemos ver o conteúdo binário dele com o mesmo <code>xxd</code>.</p>
<p>Olha como é diferente, primeiro ele não começa com <code>ELF</code> e sim com <code>MZ</code>. O cabeçalho é totalmente diferente. A gente não precisa também ficar tentando ler byte a byte do cabeçalho pra saber o que são as coisas. Todos as distribuições de Linux tem um comando chamado <code>file</code> que justamente sabe reconhecer diversos tipos de cabeçalhos de binários e identificar de uma forma que humanos consegue entender. Por exemplo, se fizermos <code>file hello</code> veja que ele identifica corretamente como um executável ELF Intel de 64-bits. Se fizermos <code>file Mesen.exe</code> vemos que identifica como um PE32 ou Portable Executable de 32-bits Intel, em particular é um assembly de .NET Mono.</p>
<p>Todo arquivo no sistema é um linguição de bytes. Se forem bytes que existem na tabela UTF-8, provavelmente é um arquivo texto, como o código do <code>hello.c</code>. Quando compilamos com o <code>gcc</code> ele gera outro linguição de bytes, mas vai ter o formato ELF que é executável de Linux. Daí quando mandamos executar <code>hello</code> ele vai identificar o cabeçalho, validar, descer até o endereço 1000 do arquivo e achar as instruções assembly pra começar a mandar pro processador. Por outro lado temos diversos outros tipos de linguição de dados, por exemplo, uma imagem JPEG que todo mundo conhece, como as fotos que seu smartphone tira.</p>
<p>Vamos abrir um arquivo com <code>xxd</code> pra ver como é. Novamente, temos um linguição de bytes, mas em particular esse começa com ff d8. Se formos até o fim do arquivo temos ff d9. Todo arquivo JPEG começa e termina do mesmo jeito. Indo até o fim, esse ff d9 indica quando o arquivo acaba porque em nenhum lugar ele indica o tamanho total. Mas voltando pro começo, a partir do byte 6 vemos que o <code>xxd</code> reconheceu quatro códigos que mapeiam na tabela ASCII pra JFIF que significa JPEG File Interchange Format. Os bytes ff d8 já identificam o arquivo como JPEG mas tem esse JFIF porque também existe o EXIF que é Exchangeable Image File Format que é um formato similar feito por fabricantes de câmeras digitais de antigamente.</p>
<p>Essa é uma das formas que o sistema operacional reconhece que tipo de arquivo estamos lidando. Se for um binário que tem um linguição de bytes que encaixa no formato ELF, é um executável e, se tiver permissão de execução, tenta executar. Se for um binário com cabeçalho de JFIF é uma imagem JPEG e um programa de ver imagens vai saber os endereços dentro que precisa pra localizar a imagem propriamente dita. E assim por diante pra todo tipo de arquivo. Zips, video, áudio.</p>
<p>E no caso de arquivos texto, eles são binários também, cujos cabeçalhos não são nenhum dos anteriores e podemos tentar mapear byte a byte pra uma tabela como UTF-8. Caso venha só um texto quebrado com lixo no meio de letras, que não parece com palavra nenhuma, pode ser um encoding diferente, digamos um texto antigo em japonês e, nesse caso, se usar a tabela EUC-JP ou Shift-JIS, podemos encontrar palavras em japonês, se o programa que estivermos usando souber usar os glifos do alfabeto japonês.</p>
<p>Por último, vale falar do problema inicial. Por que não podemos executar o código do arquivo <code>hello.c</code> diretamente sem compilar? Primeiro, ele não é um binário ELF nem PE então não contêm instruções em assembly pra passar pro processador. Mas por que eu consigo executar direto um arquivo de Python, ou Perl, ou Javascript? Isso porque normalmente ele tem duas coisas. No caso específico de Linux, vai ter permissão de execução e segundo, na primeira linha vai conter o caminho no sistema pra um interpretador. Chamamos essa linha de shebang ou simplesmente bang. Se abrir um script de Python, você vai ver que costuma ter essa primeira linha neles.</p>
<p>Se só tiver permissão de execução mas não tiver shebang, quem vai tentar executar esse arquivo texto, que agora chamamos de shebang, vai ser o shell onde estamos neste instante. Por exemplo, quando eu abro um Terminal do Arch Linux, configurei pra iniciar o Zshell. Se for um Linux normal ou um Terminal no MacOS, sem configurar nada extra, normalmente vai abrir o famoso Bash. Um Windows vem por padrão com o Powershell. Existem vários interpretadores de terminal. Eu explico mais no episódio sobre WSL 2.</p>
<p>Essa linha de comando é um ambiente de programação. Podemos escrever programinhas. Então toda vez que você cai na tal linha de comando, pode imediatamente escrever código sem abrir um editor de textos separado. Em microcomputadores antigos dos anos 80 como um MSX ou Apple II ou Commodore 64, o equivalente shell deles era o Basic. E como toda linguagem de programação ele tem uma sintaxe, funções e comandos específicos dessa linguagem que, por acaso, não tem nada a ver com a linguagem C. E por isso quando você tenta executar o <code>hello.c</code> ele vai dizer que tem erros de sintaxe. Claro, não é a mesma linguagem. Você mandou essa shell que, digamos, é brasileiro, tentar ler um texto em chinês e aí ele buga mesmo.</p>
<p>A mesma coisa acontece quando você abre o console do Python, ou um console qualquer de Javascript ou Ruby. Daí vai ser um outro shell rodando em cima do shell original, Bash ou Zshell. Todo mundo de Ruby por exemplo conhece o IRB que abre uma linha de comando onde podemos executar direto comandos de Ruby. IRB é um acrônimo pra Interactive Ruby Shell. Está no nome, ele é um shell. Mais especificamente um REPL ou Read-Eval-Print Loop, ou traduzindo, um Loop de leitura, execução e impressão. Ou seja, um programa que fica em loop infinito sempre esperando ler um comando, daí tentar executar, que é um evaluate ou <code>eval</code> desse comando, e imprimir o resultado na tela. É um shell.</p>
<p>Confunde um pouco pra quem está começando porque é difícil distinguir o que são funções nativas dos shells como Zshell ou Bash e o que são binários executáveis que parecem funções. Por exemplo, todo mundo conhece o comando <code>echo</code> que imprime o string que passamos pra ele na tela. Mas fora no Zshell, ele é um executável que fica em barra usr barra echo. Os shells tem bem poucas funções nativas, por exemplo <code>cd</code> pra mudar de diretório ou <code>alias</code> pra criar atalhos.</p>
<p>E por hoje vamos ficar por aqui. Eu acho que uma das coisas que mais facilita a vida de um programador é o quanto antes ele se tornar íntimo do computador que usa. Muitos passam anos só trabalhando com linguagens de alto nível e evitam ao máximo lidar com binários, hexadecimais, pacotes, streams, porque parece uma coisa bizarra, tipo tentar ler chinês. Só que isso limita suas capacidades como programador. E limita bastante. Ser um programador não é receber peças de Lego e sair juntando que nem um idiota, em breve é bom você mesmo saber fazer suas próprias peças de Lego. Se curtiram o video deixem um joinha e compartilhem o video com seus amigos, não deixem de assinar o canal e clicar no sininho pra não perder os próximos. A gente se vê, até mais!</p>
]]></content:encoded><category>ciencia-da-computacao</category><category>programacao</category><category>akitando</category></item><item><title>[Akitando] #87 Turing Complete, Emuladores e o Chip ARM M1</title><link>https://akitaonrails.github.io/2020/11/27/akitando-87-turing-complete-emuladores-e-o-chip-arm-m1/</link><guid isPermaLink="true">https://akitaonrails.github.io/2020/11/27/akitando-87-turing-complete-emuladores-e-o-chip-arm-m1/</guid><pubDate>Fri, 27 Nov 2020 20:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/kz3649U2sJY"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Continuando o assunto sobre máquinas de Turing hoje resolvi pegar parte do material que eu cortei do episódio anterior e aproveitar o lançamento do chip Apple M1 pra dar uma introdução ao assunto de emuladores usando aspectos das diferenças de arquitetura RISC/CISC e como ARM e x86 se comparam.&lt;/p&gt;
&lt;p&gt;Índice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;00:00 - Introdução&lt;/li&gt;
&lt;li&gt;01:10 - Turing Complete&lt;/li&gt;
&lt;li&gt;09:37 - Doom roda em QUALQUER lugar :-)&lt;/li&gt;
&lt;li&gt;11:58 - Apple M1 / ARM x x86&lt;/li&gt;
&lt;li&gt;21:40 - Pirataria&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ERRATA:&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/kz3649U2sJY"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Continuando o assunto sobre máquinas de Turing hoje resolvi pegar parte do material que eu cortei do episódio anterior e aproveitar o lançamento do chip Apple M1 pra dar uma introdução ao assunto de emuladores usando aspectos das diferenças de arquitetura RISC/CISC e como ARM e x86 se comparam.</p>
<p>Índice:</p>
<ul>
<li>00:00 - Introdução</li>
<li>01:10 - Turing Complete</li>
<li>09:37 - Doom roda em QUALQUER lugar :-)</li>
<li>11:58 - Apple M1 / ARM x x86</li>
<li>21:40 - Pirataria</li>
</ul>
<p>ERRATA:</p>
<p>No teste de gravidez, eu não prestei atenção e deixei passar que na verdade o @foone teve que trocar alguns componentes então o teste de gravidez não foi usado 100% como veio de fábrica e portanto NÃO, ele não é um computador universal também 🤷‍♂️ de qualquer forma fica o conceito, de que poderia ser, se rodasse Doom ;-) <a href="https://twitter.com/renatomefi/status/1332694946575486978?s=20"target="_blank" rel="noopener">https://twitter.com/renatomefi/status/1332694946575486978?s=20</a></p>
<p>Links:</p>
<ul>
<li>Tom Scott: Are There Problems That Computers Can&rsquo;t Solve? (<a href="https://www.youtube.com/watch?v=eqvBaj8UYz4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=eqvBaj8UYz4</a>)</li>
<li>Foone: Doom Pregnancy Test (<a href="https://twitter.com/Foone/status/1302820468819288066?s=20"target="_blank" rel="noopener">https://twitter.com/Foone/status/1302820468819288066?s=20</a>)</li>
<li>Matthew Monis: MacBook Pro 13 Review - This M1 is Insane! (<a href="https://www.youtube.com/watch?v=BUBYO591Fo8"target="_blank" rel="noopener">https://www.youtube.com/watch?v=BUBYO591Fo8</a>)</li>
<li>The Untold Story Of The Invention Of The Game Cartridge (<a href="https://www.fastcompany.com/3040889/the-untold-story-of-the-invention-of-the-game-cartridge"target="_blank" rel="noopener">https://www.fastcompany.com/3040889/the-untold-story-of-the-invention-of-the-game-cartridge</a>)</li>
</ul>
<h2>TRANSCRIPT<span class="hx:absolute hx:-mt-20" id="transcript"></span>
    <a href="#transcript" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Fiquei contente que vocês gostaram do episódio que contei um pouco mais sobre a máquina de Turing e a arquitetura de Von Neumann. Aquele episódio tinha ficado grande e eu tive que cortar um trecho enorme pra não perder a linha de raciocínio do que eu queria contar. O trecho era tão grande na real, que era praticamente outro episódio.</p>
<p>Pois é, no processo de escrever os textos e um dos motivos de porque eu não gosto de fazer live, é que eu faço muita tangente e aí o assunto fica mais complicado do que devia. Mesmo cortando eu ainda faço tangente. Ainda estou praticando cortar coisas, mas vira e mexe acontece de eu escrever 30 páginas e cortar 10 fora, como é o caso do material de hoje.</p>
<p>Então a primeira metade do video de hoje vai ser tipo a edição estendida director’s cut continuando o video anterior, mas a segunda metade vou começar a falar sobre emuladores e até dar uma palhinha do que eu acho dos novos chips Apple M1 no contexto de emulação e arquitetura. Mas isso vai ser introdução também pro próximo episódio onde vou cair mais no meu lado geek e falar de videogames.</p>
<p>(&hellip;)</p>
<p>Eu nasci no fim dos anos 70. A revolução dos micro-computadores nasceu praticamente junto comigo. Por exemplo, a Atari foi fundada em 1972, a Microsoft em 75, a Apple em 76. No mesmo ano que eu nasci também nascia o computador PET, o antecessor da linha Commodore que sairia em 82. O padrão MSX nasceu em 83. O Sinclair ZX Spectrum nasceu em 82 e no Brasil ele foi clonado pela Microdigital e virou o venerado TK 90X, que eu queria ter quando criança mas não pude.</p>
<p>No caso do TK 90X ele usava processador Zilog Z80, o mesmo também usado no Sega Mastersystem que fez muito sucesso no Brasil, fabricado pela Tectoy e que existe até hoje. Outros modelos de micros como o Apple II ou Commodore 64 ou mesmo o Atari 2600 usavam o MOS 6502 ou sua evolução, o 65C02, que eu introduzi no episódio do Guia mais Hardcore de Introdução à Computação.</p>
<p>Esse mesmo processador equipava o Nintendinho original de 8-bits. E mesmo o Super Nintendo usa o processador da família do 65816 que é baseado na arquitetura e instruções do 6502, meio como um Intel Core i5 de hoje é uma evolução dos Pentium de antigamente e ainda são compatíveis. Recomendo que assistam meu episódio também de Aprendendo sobre Computadores com Super Mario pra entender um pouco de como as instruções do 6502 funcionam na prática.</p>
<p>Essa conversa toda foi só pra voltar no conceito de máquinas Turing Universais ou UTMs. Pra recapitular, uma máquina de Turing, a abstração, é basicamente uma máquina com um leitor que consegue ler um bit de cada vez de uma fita infinita, consegue mover essa fita pra esquerda ou pra direita, e além de ler consegue mudar o valor de um bit de cada vez. Mesma coisa o console de um Nintendinho, onde é possível representar tudo que compõe o console com um número descritivo, porque ele é uma máquina de Turing.</p>
<p>Um cartucho é um conjunto de chips de ROM. Dentro dessa ROM tá gravado o programa do jogo e os dados como os sprites, imagens e áudio que compõem o jogo. Mas é basicamente tudo uma sequência de bits. Poderíamos escrever os bits todos numa fita. E de fato, antigamente a gente gravava programas e jogos em fitas cassete. Quem é dos anos 80 deve se lembrar. Era lento pra cacete, mas disk drives ainda eram muito mais caros. Mesmo um disco que tem num HD mecânico ou um CD-ROM é uma fita de discos em círculo na mídia.</p>
<p>O cartucho é como se fosse a tal fita na máquina de Turing. Mais do que isso, sem pensar que existem todos esses elementos separados, do ponto de vista da fita ou sequência de bits, são zeros e uns um atrás do outro. Se não ficou claro, vamos pegar um trecho de assembly que eu mostrei no episódio do guia de introdução à computação, instruções do 6502. Lá pelo minuto 49 do vídeo eu mostro uma sequência de instruções, lda ff, sta 60 02. Vamos considerar que o programa são só essas duas linhas. Em hexadecimal isso viraria a9 ff 8d 02 60. Em binário isso viraria esse numerozão aqui do lado. Se eu jogar na calculadora, e convertermos pra decimal, essas duas linhas de código podem ser representados pelo número trezentos e sessenta e cinco mil e tra la la que você pode ver aqui do lado. Entendeu? O programa É esse número.</p>
<p>1010 1001 11111111 1000 1101 0000 0010 1100000
365068452192</p>
<p>Zeros e uns um atrás do outro é basicamente um numerozão, em binário. No caso do cartucho do Super Mario é um numerozão de mais de trezentos e vinte mil dígitos binários ou bits, pra ser mais exato, porque são 40 kilobytes vezes um mil e vinte e quatro pra termos em bytes e vezes oito pra termos quantos bits, já que 8 bits é 1 byte. Todo programa, não importa o comprimento, é um numerozão. Todo programa que você já escreveu ou vai escrever pode ser representado por um único número gigante.</p>
<p>Não só o cartucho por ser representado por um número, mas o funcionamento do nintendinho inteiro também pode ser representado por um numerozão. Uma máquina universal de Turing pode pegar o tal número descritivo de uma máquina de Turing, como o tal numerozão que representa o Nintendinho inteiro e simular. E o resultado dessa simulação vai ser exatamente a mesma da máquina de Turing correspondente, o console de verdade. Então, uma característica importante de uma UTM é sua capacidade de &ldquo;simular&rdquo; qualquer outra máquina de Turing.</p>
<p>Resumindo. Uma máquina de Turing ou máquina de computar é um modelo abstrato capaz de ter um número finito de configurações ou estados. Ele é alimentado com uma fita infinita de bits. A cada momento a máquina tá escaneando somente um símbolo dessa fita de cada vez, e é automática porque a cada momento o estado da máquina é determinado pelo símbolo, ou bit que ele tá lendo naquele instante. A grosso modo, pense numa máquina de Turing como uma função ou método de qualquer linguagem de programação moderna.</p>
<p>O tal número descritivo representa o corpo dessa função, o binário do programa. Esse número descritivo ou programa manipula números computáveis, que na prática é quase todo número que você consegue pensar. E usa o que Turing chamou de funções computáveis. De curiosidade, ao contrário dos números, nem toda função é computável e existe muito mais funções não-computáveis do que computáveis na real. Portanto uma máquina de Turing Não consegue computar qualquer coisa. Existem funções que ela não tem capacidade de resolver.</p>
<p>Isso é um conceito importante porque a intuição popular é que dado tempo suficiente pra evolução de chips e processadores, qualquer problema deveria ser possível de resolver com um computador. Porém vale saber que existem problemas que um computador de Turing nunca vai resolver. Um exemplo disso é o Halt Problem. E como é muita tangente pra hoje, recomendo assistir a explicação do Tom Scott, que é outro canal que eu sigo faz anos. Apenas lembre disso: existem problemas que são impossíveis de resolver com um computador. E não, um computador quântico ainda é uma máquina universal de Turing. Daqui algumas décadas tem a possibilidade de ser ridiculamente mais rápido que hoje, sem dúvida, mas não vai ter mais capacidades nem vai ser capaz de resolver problemas que já não se resolve hoje.</p>
<p>De qualquer forma, uma Universal Turing Machine é uma máquina que pode pegar o número descritivo de alguma máquina de Turing e simular ela perfeitamente. Então, pra resolver problemas de computação, você pode escolher uma máquina de Turing adequada pra um problema específico, já que cada máquina só consegue executar uma tarefa. Lembrem-se que antes disso uma máquina mecânica era construída pra resolver só um tipo de problema. Agora você pode pegar uma máquina Universal, carregar o número descritivo ou programa, que é o tal numerozão que expliquei antes, e simular diversas máquinas de Turing. Esse é o conceito moderno de programação e computadores derivado dos papers de Alan Turing.</p>
<p>Na prática, computadores reais não são máquinas de Turing nem máquinas Universais, pelo simples fato que elas tem limites e a definição matemática tem o ideal de memória infinita. Mas, pra todos os efeitos e propósitos, a revolução de Alan Turing foi conseguir definir a idéia e conceitos de uma máquina de uso geral, em vez de uma máquina especializada como calculadoras por exemplo. Uma máquina de Turing seria o que chamamos de “programa”, o tal número descritivo seria a implementação desse programa, uma máquina Universal é o computador propriamente dito que consegue rodar diversos programas diferentes.</p>
<p>Alguém me perguntou uma vez se uma calculadora ou um ábaco não poderiam ser computadores já que “computam” números. E é uma pergunta válida. Tecnicamente uma calculadora “e” um computador, intuitivamente, são máquinas de calcular. E quando eu falo calculadora não estou falando de coisas como a TI-83 que todo engenheiro que se preze conhece e usa. Uma TI-83 é definitivamente um computador inteiro, que carrega programas de calcular e plotar gráficos e por isso consegue até rodar Doom nele. Estou falando daquelas calculadoras velhas que se tinha nas mesas antigamente.</p>
<p>Na minha definição informal e não-rigorosa, eu costumo pensar num computador como uma máquina que consegue simular qualquer outro computador, mesmo que bem mais lento. Performance não define um computador, suas capacidades sim. E uma linguagem Turing Complete, pra mim, é uma linguagem que consegue programar a simulação de outro computador. Se uma determinada máquina consegue rodar um emulador, pra mim é uma máquina universal. E se a linguagem de programação é capaz de escrever um emulador, pra mim ela é Turing Complete. É uma definição parcial, mas eu gosto dela porque é bem prática. Não consegue escrever um emulador, essa linguagem não é Turing Complete. Não consegue rodar um emulador, então não é um computador.</p>
<p>Recentemente um usuário que eu sigo no Twitter faz tempo, o @foone, ganhou o noticiário porque ele fez Doom rodar num teste de gravidez! Pois é, a miniaturização chegou num tal ponto que dentro de um mísero teste de farmácia, daqueles que tem uma telinha LCD bem podrona, tem um CPU turing complete, capaz de minimamente executar um jogo ao ponto dele aparecer na tela e rodar. Lento, feio, mas roda. Pára pra pensar numa coisa, todo teste de gravidez mais moderno é um computador inteiro, com um programa feito pra rodar só uma vez e depois você jogar no lixo! Isso tudo dito, não, ábaco não é um computador, a calculadora antiga de mesa não é um computador, mas um teste de gravidez como esse definitivamente é um computador.</p>
<p>Toda vez que alguém fala coisas como &ldquo;nossa, conseguiram rodar Doom numa TI-83 ou numa câmera fotográfica&rdquo; lógico, eu acho impressionante que alguém se deu a esse trabalho, mas eu não acho surpreendente porque mesmo um computador fraco é capaz de simular outro computador, só vai ser super lento. Mesma coisa quando alguém se impressiona quando vê uma distribuição Linux ou mesmo Windows 95  rodando no Chrome ou Firefox, emulado via Javascript. Não me surpreende, mas certamente impressiona, especialmente se você for iniciante.</p>
<p>Eu entendo a sensação. Eu me sentia da mesma forma nos anos 90, quando eu tinha um PC no nível de um 486 ou Pentium, que sequer tinha 32 MEGAbytes de RAM, e de repente eu tava conseguindo emular um nintendinho, que não fazia 10 anos só era possível jogar com um console de verdade. Ou a primeira vez que eu instalei o sistema operacional OS/2 que vinha com um emulador de MS-DOS e era possível até iniciar Windows 3.1 e rodar lado a lado dentro do OS/2. De qualquer forma, esses conceitos explodiam minha mente nos anos 90 e eu entendo que todo iniciante que vê isso pela primeira vez, mesmo hoje, deve ficar impressionado.</p>
<p>Agora, falando especificamente de emuladores. Um emulador simula um chip de outro computador via software. Quando um emulador carrega o jogo de um cartucho, o jogo em si não tem idéia que tá rodando num emulador. Pra todos os efeitos e propósitos, se o jogo tivesse consciência, ele não teria dúvidas que tá rodando no console de verdade. No máximo ia estranhar, “nossa, tá mais rápido esse hardware hoje”. Esse é o Matrix de verdade e eu falei um pouco disso no meu episódio sobre virtualização, dêem uma olhada.</p>
<p>Em particular sobre as discussões de ARM versus x86 que tá na moda agora, isso é um daqueles eventos que só acontece um a cada vinte anos. Eu usava Macs em 2006 quando aconteceu a transição de PowerPC pra Intel. Não vou entrar em detalhes dessas histórias porque nesse momento todo canal de tech no YouTube já tem vídeos a respeito e seria só ser redundante. Mas é relevante notar que em 2006 a Apple saiu de uma arquitetura RISC, de instruções reduzidas, pra Intel que tem instruções complexas. Agora ela tá fazendo o oposto, saindo de instruções complexas e indo pra arquitetura ARM, que é um RISC, de instruções reduzidas.</p>
<p>Pra nós programadores, uma coisa interessante é a camada Rosetta 2 que tem nos Mac OS a partir do Big Sur. Ele é um emulador pra conseguir executar binários legados de x86, convertendo as instruções pra ARM. Assim ele consegue rodar programas que não foram recompilados e atualizados pro novo chip M1. Todo reviewer tá com a cabeça explodindo porque o M1 já é mais rápido que modelos de notebooks PC comparáveis em preço aos novos Macbook Air ou Macbook Pro 13 polegadas.</p>
<p>Mais do que isso, todo mundo achava que rodar um Photoshop ou Premiere sendo emulado pelo Rosetta seria provavelmente lento. E eles tão abismados como a performance não só tá aceitável, mas tá realmente rápido, sem engasgos nem nada. Bem diferente de quando pulamos de PowerPC pra Intel em 2006. Sério, eu lembro de usar o primeiro Rosetta nos Macbook brancos Intel e rodar qualquer coisa pelo Rosetta era super lento. Dava desgosto de usar. Mas nos M1 de agora, ao que tudo indica, vai rodar quase que igual à velocidade de quando rodava no Macbook Intel.</p>
<p>Eu fiquei pensando sobre isso e vale lembrar um dos motivos possíveis. Todo mundo tá cansado de ouvir que a diferença de chips x86 e ARM é que um tem instruções mais complexas e o outro tem instruções mais simples. Pense por um segundo no que isso significa. A cada clock o processador executa uma ou mais instruções. Quanto mais instruções for possível executar por clock, maior a velocidade e menor o consumo de energia. O problema de instruções de complexidade variada é que é difícil de adivinhar quantas instruções dá pra rodar a cada clock. Pra ser mais exato, uma instrução x86 pode ser de um até 15 bytes de comprimento.</p>
<p>Por causa disso um processador Intel costuma executar algo em torno de quatro instruções por clock. Isso é horrível, porque faz de conta que chegamos numa parte do programa que tem 10 instruções simples de um ou 2 bytes um atrás do outro. Talvez o processador pudesse rodar oito, dez ou mais instruções dessas no mesmo clock. Mas como nem sempre dá pra saber se cabe ou não, ele roda só quatro. E fazendo isso ele tá desperdiçando clock, desperdiçando energia. E sem mudar a arquitetura de instruções, o pipeline sempre vai ser curto e isso é inerente a ser um x86.</p>
<p>Agora, processadores ARM tem instruções ou de 16 ou de 32 bits, ou seja, de 2 ou 4 bytes. E só. E eles não se misturam. Ou você tá executando instruções de 32-bits ou de 16-bits. Pra mudar entre um ou outro você manda uma instrução pra mudar o mode. Isso significa que você sempre sabe exatamente o tamanho das instruções e com isso pode configurar um pipeline mais longo por clock. Em particular, o chip M1 da Apple consegue executar oito instruções por clock. Só em profundidade desse pipeline, um chip M1 de 2Ghz tem potencial de ser duas vezes mais veloz e eficiente do que um chip Intel equivalente de 2Ghz. Mais do que isso, um chip M1 de 2Ghz pode ser competitivo com um chip Intel de 3 ou até 4Ghz, por isso você tanto ganho de bateria e menos calor sendo dissipado.</p>
<p>Mas não é só isso. Considere o aspecto de emulação. Pra um chip ARM emular Intel é relativamente mais fácil do que Intel emular ARM. Por que? Porque como ARM tem instruções simples, ou seja, mais básicas, ela consegue emular facilmente qualquer instrução mais complexa de Intel. Por exemplo. Pra facilitar a explicação imagina se fossem processadores gráficos. Digamos que em ARM existam funções como desenhar uma linha, desenhar um círculo, desenhar um triângulo.</p>
<p>Porém, em Intel existem funções mais complexas como, desenhar um botão, desenhar uma janela. Entenderam o que significa quando se fala em diferença de complexidade? Um só desenha linhas o outro numa única instrução já desenha um botão inteiro. Com instruções nesse ARM hipotético, como desenhar linha, eu consigo desenhar os botões ou janelas. Só vai dar mais trabalho porque eu preciso desenhar linha a linha. Se eu fosse um programador de assembly que precisa usar só essas instruções, seria um parto fazer isso. Mas como hoje escrevemos tudo em C ou outras linguagens, não faz diferença. Quem vai ter mais trabalho vai ser o compilador e não o programador. Agora, se eu só tenho instruções complexas, como desenhar o botão inteiro, como que eu desenho só uma linha? Fácil, eu faço um botão com 1 pixel de comprimento. Só que agora eu tô gastando o tempo de processamento de desenhar um botão inteiro pra fazer uma simples linha, portanto vai ser mais lento.</p>
<p>Pra piorar a situação, pra rodar instruções de x86, você precisa fazer o equivalente a passar argumentos pra uma função, como num javascript por exemplo. No caso, os argumentos você preenche em registradores e depois chama a instrução, que seria o equivalente de uma função. Ou seja, existe um número limitado de argumentos que uma instrução pode receber, porque existem poucos registradores, que funcionam como variáveis globais numa linguagem como Javascript ou PHP. Pois bem, processadores x86 como Intel de 32-bits possui só 8 registradores de uso geral. Os processadores x86 de 64-bits que a maioria de nós usa hoje tem 16 registradores de uso geral.</p>
<p>Adivinha quantos registradores tem um processador ARM de 32-bits? Dezesseis! E quantos tem o ARMv8 de 64-bits? Trinta e dois registradores. O dobro dos equivalentes x86. Ou seja, fodeu, pra um Intel emular uma instrução ARM que tem o dobro de registradores, ele vai ser obrigado a fazer vários truques pra lidar com argumentos que não cabem em todos os registradores que ele tem, e, de novo, a performance vai ser necessariamente mais baixa. No lado oposto, um chip Apple M1 que é um ARM de 64-bits, ele consegue facilmente emular qualquer instrução de x86 64 bits, porque tem registradores sobrando. Então a programação não precisa de truques, é super direto.</p>
<p>No frigir dos ovos? É uma ordem de grandeza mais simples e mais performático pra um processador ARM emular binários de x86 do que o oposto. O Rosetta de de 2006 era lento, porque ele tava justamente na situação oposta: precisava fazer os antigos processadores Intel dos novos Mac emular binários legados de PowerPC que é uma arquitetura RISC como ARM, era o caso de ter que usar uma função complexa que desenha um botão e desperdiçar o processamento pra emular uma linha.</p>
<p>É um dos motivos de porque é mais difícil ter emuladores de processadores RISC rodando bem em cima de um processador Intel. Por exemplo, até hoje não é performático emular os antigos PowerPC pra rodar Mac antigo. Da mesma forma é lento emular chips de smartphone, como os da Qualcomm que tem na maioria dos celulares Android que usamos. Já o inverso é mais rápido. Por isso eu fiquei um pouco decepcionado com o processador SQ1 que a Microsoft criou em parceria com a Qualcomm pra equipar os Surface Pro X.</p>
<p>O SQ1 é um Apple M1 piorzinho. Também é um chip ARM e tá rodando Windows 10 compilado pra ARM, como o Big Sur no caso do Mac. E pra rodar programas legados de x86, ele também roda um emulador. Até hoje tá limitado a rodar binários de 32-bits e mesmo assim roda muito lento. Tá pra sair o suporte a emular binários de 64-bits, e a expectativa é que seja um pouco melhor, mas mesmo assim ainda atrás da Apple. Talvez você nem tenha ouvido falar do Surface Pro X, mas não é de hoje que a Microsoft vem tentando usar chips ARM também. A Apple não foi a primeira, mas pra variar, foi a que entregou mais bem feito.</p>
<p>O fato de emular x86 em cima de ARM não ser tão difícil e não representar uma perda tão significativa de performance é importante, porque com Windows, MacOS e Linux ganhando cada vez mais otimizações pra ARM, significa que a hegemonia da Intel tá com os dias contados. Por causa dos smartphones a tecnologia ARM evoluiu bastante e em ambiente móvel, a Intel simplesmente não tem como competir. E agora esses processadores tão pulando pra notebooks e desktop, e já tão bons. E com emuladores ninguém vai ficar preso em Intel por causa de aplicativos legados. E se esse caminho der certo, em breve esses chips ARM vão invadir em massa o ambiente de servidores. Aliás, o maior supercomputador do mundo já usa ARM. E isso poderia representar o último prego no caixão da Intel. Ela vai precisar se mexer rápido pra evitar isso.</p>
<p>Eu vou falar um pouco mais sobre ARM no próximo episódio, mas de qualquer forma a comunidade de desenvolvimento de emuladores é grande. Eu sempre fico impressionado porque na maior parte são voluntários trabalhando só pelo prazer de ver as coisas funcionando mesmo, já que não se pode ganhar muito dinheiro em cima de jogos que ainda tem licenças e copyrights, especialmente se mexer em consoles da Nintendo. Nunca mexa com a Nintendo, é processo garantido e muita diversão pra se livrar. A única exceção é se você vive na China, daí você tá seguro.</p>
<p>Não dá pra falar de emuladores e não tocar no assunto de pirataria. É um assunto controverso, ser a favor ou contra direitos autorais e eu vou dizer que sou neutro nessa discussão. Não tenho uma solução completa nem pra um lado, nem pro outro. Então não vou tentar argumentar hoje. Mas sim, como todo mundo, eu já pirateei muito quando era moleque. Eu teria tido muito mais dificuldade pra começar a ganhar dinheiro se não fosse pela pirataria naquela época, então tem esse ponto mesmo. Hoje em dia não tenho esse problema, então não tenho motivos pra piratear e pago tudo que eu uso, acho justo.</p>
<p>Mesmo assim, existem software antigos especialmente de lugares como Japão por exemplo, que mesmo eu querendo comprar, não existe pra vender, e nesse caso a pirataria tem a função de preservar software do passado. E isso é outro fator importante. Então, se você é a favor ou contra, não sou eu que vou te julgar. Isso tudo dito, assim como todo YouTuber que fala do assunto, eu não vou dizer onde você encontra jogos pirata, mas qualquer um que saiba usar Google e sabe diferenciar de um malware, vai achar o que procura sem muito trabalho.</p>
<p>E por hoje é isso aí! Já divaguei bastante e vai ter mais nerdice no próximo episódio. Eu sei que o assunto do Apple M1 tá na cabeça do povo, mas não pretendo fazer um video só sobre isso porque já tem bastante, mas de vez em quando vou salpicando algumas curiosidades como fiz hoje e vou fazer no próximo. Se ficaram com dúvidas não deixem de mandar nos comentários, se curtiram o video mandei um joinha e compartilhem o video com seus amigos, assinem o canal e não deixem de clicar no sininho pra não perder os próximos videos. A gente se vê, até mais!</p>
]]></content:encoded><category>ciencia-da-computacao</category><category>emulacao</category><category>hardware</category><category>akitando</category></item><item><title>[Akitando] #86 - O Computador de Turing e Von Neumann | Por que calculadoras não são computadores?</title><link>https://akitaonrails.github.io/2020/10/23/akitando-86-o-computador-de-turing-e-von-neumann-por-que-calculadoras-nao-sao-computadores/</link><guid isPermaLink="true">https://akitaonrails.github.io/2020/10/23/akitando-86-o-computador-de-turing-e-von-neumann-por-que-calculadoras-nao-sao-computadores/</guid><pubDate>Fri, 23 Oct 2020 10:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/G4MvFT8TGII"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Quem foi John Von Neumann? Como ele e Alan Turing inventaram o computador moderno? E como uma máquina de escrever levou Alan Turing ao computador? E o que é esse tal de &amp;ldquo;Turing Complete&amp;rdquo; que toda discussão de linguagens de programação menciona?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;00:00 - Intro&lt;/li&gt;
&lt;li&gt;01:24 - Alan Turing e sua máquina de escrever&lt;/li&gt;
&lt;li&gt;06:31 - O que são Máquinas de Turing?&lt;/li&gt;
&lt;li&gt;15:41 - de Babbage, a Zuse, ao Colossus&lt;/li&gt;
&lt;li&gt;20:17 - O que é Turing Complete?&lt;/li&gt;
&lt;li&gt;30:03 - O maior gênio que já viveu: Von Neumann&lt;/li&gt;
&lt;li&gt;39:35 - Turing + Von Neumann = Computador Moderno&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Links:&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/G4MvFT8TGII"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quem foi John Von Neumann? Como ele e Alan Turing inventaram o computador moderno? E como uma máquina de escrever levou Alan Turing ao computador? E o que é esse tal de &ldquo;Turing Complete&rdquo; que toda discussão de linguagens de programação menciona?</p>
<ul>
<li>00:00 - Intro</li>
<li>01:24 - Alan Turing e sua máquina de escrever</li>
<li>06:31 - O que são Máquinas de Turing?</li>
<li>15:41 - de Babbage, a Zuse, ao Colossus</li>
<li>20:17 - O que é Turing Complete?</li>
<li>30:03 - O maior gênio que já viveu: Von Neumann</li>
<li>39:35 - Turing + Von Neumann = Computador Moderno</li>
</ul>
<p>Links:</p>
<ul>
<li>Boy Who Wanted to &lsquo;Make a Typewriter&rsquo; Instead Became Father of Computer Science (<a href="https://oztypewriter.blogspot.com/2013/09/boy-who-wanted-to-make-typewriter.html"target="_blank" rel="noopener">https://oztypewriter.blogspot.com/2013/09/boy-who-wanted-to-make-typewriter.html</a>)</li>
<li>What exactly is Turing Completeness? (<a href="https://medium.com/@evinsellin/what-exactly-is-turing-completeness-a08cc36b26e2"target="_blank" rel="noopener">https://medium.com/@evinsellin/what-exactly-is-turing-completeness-a08cc36b26e2</a>)</li>
<li>Are There Problems That Computers Can&rsquo;t Solve? (<a href="https://www.youtube.com/watch?v=eqvBaj8UYz4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=eqvBaj8UYz4</a>)</li>
<li>Turing Machine Visualization (<a href="https://turingmachine.io/"target="_blank" rel="noopener">https://turingmachine.io/</a>)</li>
<li>Turing Complete (<a href="https://wiki.c2.com/?TuringComplete"target="_blank" rel="noopener">https://wiki.c2.com/?TuringComplete</a>)</li>
<li>Who Invented the Computer? (<a href="https://www.turing.org.uk/scrapbook/computer.html"target="_blank" rel="noopener">https://www.turing.org.uk/scrapbook/computer.html</a>)</li>
<li>Konrad Zuses Z3 in Detail (<a href="http://www.inf.fu-berlin.de/lehre/SS01/hc/zuse/node4.html"target="_blank" rel="noopener">http://www.inf.fu-berlin.de/lehre/SS01/hc/zuse/node4.html</a>)</li>
<li>The Unparalleled Genius of John von Neumann (<a href="https://medium.com/cantors-paradise/the-unparalleled-genius-of-john-von-neumann-791bb9f42a2d"target="_blank" rel="noopener">https://medium.com/cantors-paradise/the-unparalleled-genius-of-john-von-neumann-791bb9f42a2d</a>)</li>
<li>Plankalkül (<a href="https://en.wikipedia.org/wiki/Plankalk%C3%BCl"target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Plankalk%C3%BCl</a>)</li>
<li>PC Jr (<a href="https://www.pcjs.org/"target="_blank" rel="noopener">https://www.pcjs.org/</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>No final da segunda parte do video sobre teclados, quem assistiu viu que mostrei o avô dos teclados mecânicos, a máquina de escrever de verdade. E quando eu adicionei ele no video eu tinha dois objetivos. O primeiro, claro, era adicionar uma camada de história naquele video, mas o segundo era usar de gancho pra historinha que quero contar hoje. Aliás, historinha nada, se prepara que essa vai ser uma história bem longa e se você nunca leu sobre Turing e Von Neumann, você vai finalmente entender o que é um computador e porque uma máquina de calcular não é um computador.</p>
<p>Além disso este episódio vai servir como ponte entre os vídeos do guia hardcore de introdução a computação com o Nintendinho que mostrei alguns vídeos atrás, e o próximo que vai ser mais leve e quero falar um pouco mais sobre meus video games e emuladores preferidos. Quero explicar como que de uma máquina de escrever foi possível chegar no computador moderno. E quero contar um pouco da história das duas pessoas que eu considero mais importantes e que representam o tipo de gigantes que construíram as bases da computação moderna. Vamos focar em duas dessas pessoas, uma é famosa e você provavelmente conhece. O outro é um dos maiores gênios que já existiu, um monstro de proporções bíblicas, pau a pau ao intelecto de um Albert Einstein, mas que você provavelmente nunca ouviu falar.</p>
<p>(&hellip;)</p>
<p>Como disse na introdução, a história que vou contar agora eu tô segurando desde quando fiz a série sobre teclados. Quem assistiu vai se lembrar da máquina de escrever mecânica original de Christopher Sholes. O que vocês não sabem é que a máquina de escrever teve um papel crucial na invenção do computador moderno. Não lembro como mas anos atrás eu esbarrei num artigo muito bom do blog de um cara chamado Robert Messenger, de 2013, e eu vou pegar bastante coisa dele.</p>
<p>O artigo começa assim: em vinte e três de junho de 1912, no quadragésimo quarto aniversário da primeira patente de máquina de escrever de Christopher Latham Sholes, uma data celebrada até hoje como o Dia da Máquina de Escrever - Ethel Stoney Turing, filha do engenheiro chefe da Madras Railways, deu a luz em Paddington. Um filho. Onze anos depois, no verão de 1923, esse menino, Alan Mathison Turing escreveu a seus pais a partir da sua escola em Hazelhurst, Sussex. Os pais estavam na India porque o pai, Julius Mathison Turing trabalhava no serviço civil da India. Ele contava que naquela semana ele imaginou como fazer uma máquina de escrever..</p>
<p>Alan Turing nunca inventou uma máquina de escrever, mas usou de inspiração pra se tornar o pai da Ciência da Computação. Talvez vocês se lembrem do filme The Imitation Game, onde o eterno Sherlock Holmes, Benedict Cumberbatch, interpreta Alan Turing no famoso episódio da Segunda Guerra Mundial, onde ele derrota a máquina de criptografia Enigma dos alemães com uma máquina de computar enorme, um passo importante pra ajudar terminar a guerra a favor dos Aliados. Se ainda não assistiu o filme, recomendo, embora ele não explique de fato como funciona a máquina de Turing, pra isso recomendo ver meus videos sobre criptografia.</p>
<p>(mostrar maquina)
O filme foi baseado no livro Alan Turing: The Enigma, de Andrew Hodges, que foi publicado em 1983. Num dos trechos dessa biografia temos a passagem que diz que a mãe de Turing tinha uma máquina de escrever e que isso fascinava o jovem Alan. Ele desenhou sua máquina automática ou a-machine como um tipo idealizado de máquina de escrever com carriage, ou carro, infinito, sobre a qual uma cabeça de leitura passava com habilidade não só de escrever, mas de ler e apagar um quadrado de cada vez antes de mover pro quadrado imediatamente adjacente.</p>
<p>A biografia segue explicando que, claro, existiam máquinas que manipulavam símbolos naquela época, a máquina de escrever era um exemplo. Alan sonhou em inventar máquinas de escrever quando criança. Sua mãe tinha uma e ele pode ter começado se perguntando o que significava dizer que uma máquina é &ldquo;mecânica&rdquo;. Podia significar que sua resposta, pra alguma ação de um operador, era perfeitamente previsível. Esquece por um momento que você já conhece computadores, e pense numa época que só se conhecesse máquinas como a de escrever.</p>
<p>Alguém poderia descrever de antemão exatamente como a máquina se comportaria sob qualquer contingência. Mas havia mais a ser dito sobre a humilde máquina de escrever do que isso. A resposta depende das condições da máquina, ou o que o Alan chamava da &ldquo;configuração&rdquo; atual da máquina. Em particular, uma máquina de escrever poderia estar na configuração de maiúscula ou  minúscula.</p>
<p>Essa era uma idéia que o Alan elaborou de forma mais genérica e abstrata. Ele considerou máquinas que em qualquer determinado momento estariam em uma de um número finito de possíveis configurações. Então, se existe somente um número finito de coisas que a máquina pode fazer, é possível saber todo o comportamento da máquina de uma forma finita.</p>
<p>Entretanto, a máquina de escrever tinha uma funcionalidade a mais que era essencial à sua função. Seu ponto de digitação se mexia, relativo à página. Sua ação de digitação seria independente da posição desse ponto na página. Ela teria &ldquo;configurações&rdquo; internas e uma posição variável na linha de impressão. A ação da máquina não dependeria da sua posição.</p>
<p>Sem levar em consideração detalhes como margem, linha de controle e assim por diante, o que eu acabei de listar seria suficiente pra dar uma descrição completa da natureza de uma máquina de escrever. Um relato exato das configurações e posições permitidas e de como teclas determinam os símbolos impressos, tecla de shift pra mudar configuração de minúscula pra maiúscula, a barra de espaço e backspace, tudo isso resumiria as funcionalidades mais relevantes.</p>
<p>Se um engenheiro olhasse essa descrição e criasse uma máquina física que atendesse a essas especificações, o resultado seria uma máquina de escrever, independente da cor, peso ou outros atributos. Mas uma máquina de escrever era muito limitada pra servir de modelo. Ela lidava com símbolos, só conseguia escrever, e precisava de um operador humano pra escolher os símbolos e mudar as configurações de posição, uma de cada vez. Então Turing se perguntou, o que seria um tipo “mais” genérico de máquina que lidasse com símbolos?</p>
<p>Pra ser uma &ldquo;máquina&rdquo; ela precisaria reter a qualidade de uma máquina de escrever, de ter um número finito de configurações, e um comportamento determinado exato em cada. Mas deveria ser capaz de muito mais. Então ele começou a imaginar máquinas que seriam super-máquinas de escrever.</p>
<p>Pra simplificar, ele imaginou máquinas trabalhando só em uma linha. Isso é uma tecnicalidade, mas que permitia esquecer de coisas como margens e linhas de controle. Mas era importante que o suprimento de papel pudesse ser, tipo, infinito. Nessa imagem, o ponto de digitação de sua super máquina de escrever poderia progredir pra esquerda ou pra direita indefinidamente. Por uma questão de definição, ele imaginou esse papel sendo uma forma de fita, marcado com unidades quadradas, de tal forma que somente um símbolo pudesse ser escrito em cada uma. Então suas máquinas teriam uma definição finita, mas poderiam trabalhar em uma quantidade infinita de espaço.</p>
<p>E assim foi nascendo as sementes da idéia de uma máquina discreta universal, ou seja, que pudesse ser configurada pra várias tarefas diferentes. A máquina de escrever de Sholes, reduzida aos seus princípios fundamentais, nos suporta até hoje. Turing simplesmente se livrou das pessoas e digitadores. E isso é possível porque a máquina de Turing é ainda mais excepcionalmente crua do que uma máquina de escrever. Tudo que ele usa é uma fita de papel que é ao mesmo tempo material de programa e dados, seu input e output ao mesmo tempo.</p>
<p>Turing enxugou a máquina de escrever a essa fita. Mas tem ainda mais economias, sua máquina não precisa de letras redundantes, cifras e sinais de um teclado de máquina, ele consegue se virar somente com um sinal e a ausência desse sinal, ou seja, 1 e zero. Essa informação binária pode ser lida ou escaneada pela máquina. Daí ela pode mover a fita de papel um espaço pra direita, um pra esquerda, ou não se mexer.</p>
<p>E é só isso. Sério, isso que acabei de descrever é um computador. E nenhum computador que já foi construído ou vai ser construído vai conseguir fazer mais que esse modelo. Isso foi provado matematicamente por Alan Turing em 1936. E resumindo, uma máquina de Turing conforme definido no seu paper On Computable Numbers, with an application to the Entscheidungsproblem, são construções matemáticas abstratas que nos ajudam a descrever rigorosamente o que significa computação.</p>
<p>Se você não entendeu ainda o que é isso de fita infinita e cabeça de leitura, eu achei um site chamado Turing Machine Visualization que permite escrever uma máquina de Turing usando uma simplificação da notação que ele usou no seu paper. No paper, Turing define sua máquina como tendo um ou mais estados, cuja notação é esse Quezão, daí um estado inicial q0 pertencente a Quezão, algum parâmetro de entrada Sigma, a tal fita Gamma, onde Sigma que é o parâmetro está contido na fita Gamma, um símbolo branco bezinho pertencente à fita Gamma, e uma ou mais funções de transição Delta que tem o tipo indo de Quezão e Gamma para Gamma, left ou right ou seja, comando pra esquerda ou pra direita e Quezão.</p>
<p>Ver notação matemática é um troço que faz o cérebro da maioria desligar em pânico, mas relaxa que vai ficar mais simples já já. Vamos considerar uma máquina de Turing muito simples de exemplo, que faz um incremento em um número binário, basicamente soma 1 a um número. A gente viu isso no video do guia hardcore de introdução à computação, então você deve se lembrar. Vamos implementar usando essa notação.</p>
<p>Começamos definindo os estados possíveis nessa lista Quezão, temos direita, sobe um, e término. O processo começa no estado inicial q0 sendo ir pra direita. O número de entrada em binário é o Sigma igual a 1 0 nesse exemplo, mas poderia ser qualquer número em binário. A fita Gamma contém o Sigma então ele tem 1 0 e espaço em branco. E o bezinho define o espaço em branco.</p>
<p>Agora definimos as funções de transição. Se o estado for pra direita e a cabeça estiver lendo 1 na fita ele devolve 1, ou seja, não muda nada, vai pra direita e mantém o estado em ir pra direita. Mesma coisa pro zero. Porém, se ele ler um espaço em branco, não escreve nada mas agora muda o comando pra ir pra esquerda e muda o estado pra carry, ou sobe um.</p>
<p>Se o estado for sobre um e ele ler 1, agora apaga e escreve zero no lugar, em seguida dá comando pra continuar pra esquerda e mantém o estado em sobe um. Agora, se for zero, escreve 1, dá comando pra esquerda e muda o estado pra done ou término. Mesma coisa se ler espaço em branco.</p>
<p>Eu sei, até agora muitos de vocês provavelmente não estão entendendo nada, mas aguenta aí. Indo pro site que eu recomendei, o Turing Machine Visualization, podemos reescrever dessa notação mais formal pra uma mais prática e mais parecida com uma linguagem de programação. Eles fizeram um interpretador pra facilitar a gente escrever o equivalente a programinhas. Se você é programador, isso deve ser super trivial.</p>
<p>Então vamos trocar a notação matemática por &hellip; isto! Agora deve estar assustando menos né? Recapitulando, começamos definindo o equivalente a uma variável com o número binário que queremos incrementar, esse é tipo o argumento ou parâmetro de entrada. Daí tem esse blank que define o que é um espaço em branco. Temos start state sendo right que é o estado inicial de indo pra direita.</p>
<p>Em seguida temos a tabela de configuração de transições. Se você alguma vez já trabalhou num sistema de e-commerce por exemplo, já deve ter visto uma máquina de estados que define o estado de uma ordem de venda, por exemplo. Se eu vou na Amazon e compro um Kindle, meu pedido pode iniciar com um estado de aguardando pagamento e dá o comando de executar o pagamento. Quando o pagamento é confirmado o estado do pedido muda pra, por exemplo, aguardando envio, e ele solta um comando pra mandar pra fila do centro de distribuição. E assim por diante até o pedido ter confirmação da entrega.</p>
<p>Como é um conjunto finito de estados, chamamos essa estrutura de estados e transições de máquinas de estado finito ou autômatos finitos. Isso se estuda em Teoria dos autômatos, que é um sub-tópico de ciências da computação. Na prática você define diferentes estados que seu sistema pode estar, e define o que acontece na transição de um estado pra outro. Qualquer linguagem moderna tem bibliotecas pra lidar com isso. Mas de qualquer forma, a máquina de Turing original é configurada com uma tabela de estados e transições. No exemplo do site ela pode estar no estado de &ldquo;right&rdquo; ou o que eu chamaria de &ldquo;indo pra direita&rdquo; ou &ldquo;carry&rdquo; que seria &ldquo;subindo um&rdquo;. Subindo um no sentido do sobe um que fazemos em somas.</p>
<p>O primeiro estado é muito simples, ele simplesmente vai indo pra direita sem fazer nada enquanto for lendo 1 ou 0 na fita. Mas ele pára quando encontra um espaço em branco. Nesse caso ele muda o estado pra subindo um e recebe o comando de voltar pra esquerda.</p>
<p>No estado de subindo um, se encontrar 1 ele escreve 0, porque vai subir um pra esquerda. E vai trocando 1 por 0, 1 por 0, até encontrar 0. Daí ele pode colocar o 1 que tinha subido e terminar, já que esse programa é simplesmente um incremento de 1. E daí ele muda o estado pra &ldquo;done&rdquo; ou terminado. Nesse site você vai encontrar exemplos bem mais complexos que isso, por exemplo, pra determinar se um número é divisível por três. Ou melhor ainda, nós já vimos no outro video como fazer uma máquina de adição de números binários com o processador MOS 6502 num protoboard. Eis a versão de autômatos finitos numa máquina de Turing. Você pode ir avançando passo a passo pra ver o sistema funcionando visualmente, então é bem fácil de entender. Divirtam-se.</p>
<p>Na prática você poderia pensar no tal ponto de digitação que falei antes ou “cabeça” como a cabeça de leitura de um disquete, ou CD, ou Blu Ray, e a fita como sendo a cadeia de bits que tem nas trilhas. Nós sempre lidamos com dados e programas da mesma forma, como uma cadeia super longa de bits. Alguns dos bits são instruções que executamos como programa, outros bits são dados como um texto, uma imagem ou um video. E se for um HD, podemos escrever também. Além disso essa cabeça pode se movimentar pra trás ou pra frente, ou seja, ela pode ter acesso aleatório a qualquer posição na fita. E por exemplo, você vai se lembrar que RAM quer dizer exatamente isso, memória de acesso aleatório.</p>
<p>A beleza do trabalho de Turing foi ter definido com rigor matemático o que é um programa, na forma da máquina de Turing, configurado como transições entre estados finitos. E o conceito de uma máquina universal que consegue executar quaisquer máquinas de Turing, que é o que ele chama de máquina de Turing Universal e na prática chamamos isso de computador. Uma máquina Universal de Turing é uma máquina de Turing cuja tarefa é simular outra máquina de Turing arbitrária, com uma entrada de dados arbitrária.</p>
<p>O que chamamos de computador moderno tem uma característica muito importante: ela consegue carregar e armazenar configurações, ou o que chamamos de programas.  Essa é uma distinção que eu considero importante. Recentemente me perguntaram isso: o que diferencia um ábaco ou uma calculadora mecânica de um &ldquo;computador&rdquo; de verdade? A definição é se ela é ou não é uma máquina universal de Turing, particularmente com a característica de não distinção entre programa e dados no armazenamento, na tal fita infinita, o que permite a universalidade dele poder simular outra máquina de Turing.</p>
<p>Antes de Turing existem diversos experimentos que alguns chamam de &ldquo;computador&rdquo; mas que na realidade seria mais correto chamar de máquinas de calcular grandes. E aqui eu vou usar trechos do artigo &ldquo;Quem Inventou o Computador&rdquo; do site do próprio autor da biografia do Turing, o Andrew Hodges. Ele diz, e eu concordo, por exemplo, que não podemos chamar o Engenho Analítico de Charles Babbage de computador. Ele não incorpora a idéia vital de armazenar programas da mesma forma que os dados.</p>
<p>A máquina de Babbage foi desenhada pra armazenar programas em cartões perfurados, enquanto o trabalho de calcular era feito mecanicamente por engrenagens e rodas. Havia outras diferenças, claro, como ainda usar base numérica decimal em vez de binário, ou obviamente não ser eletrônico já que sequer Thomas Edison tinha nascido naquela época.</p>
<p>Cem anos depois, em 1940, dava pra usar relays eletromagnéticos no lugar de engrenagens. Mas ninguém avançou além dos princípios da Babbage. Existiam grandes calculadoras que você podia reconfigurar mas nenhum deles era um computador. A idéia era a mesma: você construía uma máquina que fazia aritmética, e então rearranjava as instruções encodadas num cartão perfurado ou mudando cabos e switches de lugar ou algo assim, e fazia a máquina calcular. Depois que terminasse e precisasse fazer outro cálculo, precisava reconfigurar a máquina inteira de novo. Eram calculadoras grandes reconfiguráveis, mas não eram computadores. De novo, elas não tinham como armazenar programas e não eram capazes de simular qualquer outra máquina.</p>
<p>Pelo mesmo motivo, o ENIAC que muitos consideram um dos primeiros computadores também não era um computador. Era outra calculadora gigante. Sim, só porque era grande e complexa usava válvulas no lugar de engrenagens, não é isso que faz a máquina ser um computador. Durante a guerra teve várias tentativas de quebrar a criptografia alemã e um deles foi a máquina batizada de Colussus, na Inglaterra. Novamente, não era um computador, era uma máquina especificamente desenhada pra quebrar ciphers da máquina de Lorenz. E por mais complexa e útil que ela fosse, ainda não era um computador.</p>
<p>O papel mais importante do Colossus foi de fato mostrar pra Alan Turing, que também tava trabalhando pra quebrar a criptografia alemã, sobre a velocidade e confiabilidade da eletrônica. E esse aparato britânico tava na frente dos esforços norte-americanos já que o ENIAC tinha tamanho e complexidade comparáveis ao Colossus, mas só começou realmente a funcionar depois que a guerra tinha acabado, em 1946. E aí ela já tava obsoleta.</p>
<p>O engenheiro alemão Konrad Zuse desenhou calculadoras mecânicas e eletromecânicas de forma independente também, antes e durante a guerra. Ele não usou eletrônicos. E o que ele construiu ainda era mais próximo dos princípios de Babbage, mas teve o mérito de reconhecer a importância da programação e pode ser creditado por um tipo de linguagem, o Plankalkül.</p>
<p>Zuse foi um caso de azar. Ele era um gêniozinho também, autodidata, fazia tudo sozinho. Pra descrever circuitos lógicos ele inventou um sistema próprio de notação de diagramas. Depois de acabar o Z1 em 1938 ele descobriu que o cálculo que inventou sozinho já existia e era conhecido como cálculo proposicional. Enquanto trabalhava na sua dissertação de doutorado, ele desenvolveu o primeiro sistema formal conhecido pra notação de algoritmos, capaz de lidar com branches e loops. Ele descreveu sua linguagem Plankalkül num livro que não foi publicado, porque foi bem no período conturbado quando caiu a Alemanha Nazista. Os computadores que construiu foram destruídos por bombas durante a guerra, ele só conseguiu resgatar o Z4.</p>
<p>O Z3 também é considerado por muitos como o primeiro computador programável. Um ENIAC precisava ser reprogramado pra cada tarefa via o trabalhoso, tedioso e fácil de errar mecanismo via conexão de dezenas de cabos. E diferente do monstruoso ENIAC ou mesmo do Colossus da Inglaterra, foi o Z3 feito na unha por Zuse, sem financiamento do governo ou institutos nem nada, quem chegou mais perto do que Turing viria a definir como máquina de Turing. O problema é que o Z3 não tinha condicionais, mesmo assim, pelo menos na teoria, ele quase poderia ser considerado Turing Complete.</p>
<p>Nas palavras de Raúl Rojas que em 1998 demonstrou isso, ele diz &ldquo;Nós podemos portanto dizer que, de uma perspectiva abstrata teórica, o modelo de computação do Z3 é equivalente ao modelo de computação dos computadores de hoje, mas de uma perspectiva prática, e do jeito que o Z3 é realmente programado, ele não equivale ao modelo moderno de computador&rdquo;.</p>
<p>Aliás, assim como Raúl demonstrou, existem diversas formas artificiais e bem tortuosas de tentar montar experimentos teóricos abstratos que no caso extremo dá pra dizer que um Engenho Analítico de Babbage, o Z3 de Zuse ou o Colossus tem &ldquo;potencial&rdquo; de ser Turing Complete, mas na prática eles não tinham chegado lá de verdade. Eu disse que o caso de Zuse foi de azar porque sendo conterrâneo de Turing a diferença entre os dois é que Turing foi escolhido pelo governo Britânico pra ajudar nos esforços da guerra e teve a sorte de trabalhar num projeto de grande notoriedade, mas quando Zuse se voluntariou pra ajudar na guerra, ele foi rejeitado.</p>
<p>Antes de continuar a história eu falei algumas vezes o termo Turing Complete. E se você é programador faz algum tempo, já deve ter visto ou participado de discussões inúteis sobre se uma linguagem é Turing Complete ou não. Deixa eu aproveitar pra esclarecer isso.</p>
<p>Máquinas de Turing podem ser funções parciais, ou seja funções pras quais não tem saídas bem definidas. Elas podem receber qualquer número inteiro e dar de saída qualquer número inteiro. No paper dele tem uma definição bem mais formal do que isso, mas só imagine uma máquina de Turing como uma função numa linguagem moderna, que recebe números e devolve números. E o conjunto de funções que você pode expressar com máquinas de Turing é o que chamamos de funções computáveis.</p>
<p>Existem números computáveis e também números não-computáveis. Se for possível calcular um número com precisão arbitrária, esse número é computável. No universo de todos os números possíveis, de inteiros a complexos a pi e tudo mais, nem todos são computáveis, mas na prática é difícil de cabeça chegar num número não computável. A maioria que mexemos no dia a dia são computáveis. Mas isso é só pra relembrar que computadores não necessariamente conseguem lidar com 100% dos números possíveis na matemática. No máximo aproximamos.</p>
<p>Pra um programador que não é cientista da computação e não vai lidar no dia a dia com construção de CPUs ou linguagens de computador, talvez as definições formais não sejam tão interessantes, por isso não estou detalhando demais. Mas é mais pra entender que existe uma definição matemática que delimita o que são números computáveis e funções computáveis, ou seja, existe uma definição não só do que dá pra fazer, mas também do que não é computável.  Se quiser se aprofundar, o conceito mais importante sobre isso é o que Turing chamou de sequências computáveis. Fica de lição de casa pra vocês pesquisar o que é isso.</p>
<p>Mas voltando pra definição, uma linguagem Turing complete é uma que pode simular uma máquina de Turing, como no site de visualização. Só isso. Ou seja, se você pode simular uma máquina de Turing, então pode computar qualquer função que uma máquina de Turing consegue computar, mas o que a torna &ldquo;completa&rdquo; é que ela pode computar todo o conjunto de funções computáveis de Turing, e não só algumas. Na prática, todas as linguagens de programação que você consegue imaginar provavelmente podem ser &ldquo;consideradas&rdquo; Turing Complete na prática.</p>
<p>Se fôssemos rigorosos ao pé da letra, acho que nem existem linguagens que são Turing Complete.Por definição uma máquina de Turing tem uma memória, a tal fita de papel, que não tem limites pra esquerda ou pra direita, é infinita. Um computador de verdade obviamente tem limites. Você nunca vai ter RAM infinita. Ela pode ser muito grande, ter peta-petabytes, mas isso não tá longe, mas muito longe de infinito. Máquinas ou linguagens que conseguem fazer tudo que uma máquina de Turing consegue mas até um certo limite de memória, chamamos de Linear Bounded Automations ou Automação Linear Limitada, que são máquinas de Turing que impõe um limite pra esquerda e pra direita da fita.</p>
<p>E a pergunta mais óbvia é, se um computador não pode ser 100% uma máquina de Turing, como uma linguagem de programação, que roda num computador com limites, poderia ser Turing Complete? Tem um jeito, que é definir uma linguagem só no papel dizendo que suporta Listas ou Arrays infinitos, mas na prática a implementação que limita o tamanho máximo.  Ou seja, tipo dizer que na “intenção” ele tá dentro das regras, mas quando tenta rodar, o hardware que é limitado e não deixa acessar memória infinita e crasheia. É um truquinho sujo usando uma tecnicalidade da definição.</p>
<p>Na formalidade matemática isso faz diferença. Mas pra um engenheiro faz pouca diferença. Então, se você encontrar alguma discussão acirrada se a linguagem X ou Y é ou não é Turing Complete e ninguém tá provando que as tais linguagens conseguem de verdade lidar com memória sem limites, então a discussão é uma grande perda de tempo.</p>
<p>Na prática, toda linguagem de programação que chamamos por aí de Turing Complete tem capacidade de fazer qualquer coisa que outra linguagem Turing Complete consegue fazer. Dado tempo suficiente de execução e memória suficiente, elas são equivalentes. Agora, ser capaz não é a mesma coisa que ser eficiente. Muita gente confunde isso. Por isso é possível e não deveria ser surpresa, fazer um simulador de PC antigo em Javascript, como nesse site chamado PC Junior que vou deixar linkado nas descrições.</p>
<p>Se você ainda não entendeu, um PC é uma máquina universal de Turing, que consegue rodar um simulador de PC, que é uma máquina de Turing, feita numa linguagem Turing Complete como Javascript, pra simular outras máquina de Turing, no caso um PC virtual pra jogar Doom. Como os PCs de hoje são exponencialmente mais poderosos que PCs dos anos 90, até usando uma linguagem como Javascript conseguimos simular o PC velho inteiro a ponto de rodar Windows 95, Windows 3.1, jogos como Commander Keen ou Doom, e esses programas não sabem que estão rodando num ambiente simulado.</p>
<p>De qualquer forma, pra uma linguagem ser Turing Complete na prática, não precisa de muita coisa. Não são sintaxes exóticas, bibliotecas complicadas nem nada disso. Numa linguagem imperativa basta ter alguma forma de repetição condicional como um &ldquo;while&rdquo; ou um &ldquo;if com go to&rdquo; e uma forma de ler e escrever pra algum lugar, tipo registradores ou variáveis. E numa linguagem funcional, baseada em lambda calculus, basta conseguir abstrair funções de argumentos e aplicar funções em argumentos. Tecnicamente, se você consegue fazer recursão, tá com meio caminho andado.</p>
<p>Existe um conceito chamado Turing tarpit ou poço de piche, no sentido de você se sujar e afundar, de linguagens que foram desenhadas especificamente pra terem só o mínimo do mínimo pra ser Turing Complete. Ser legível e fácil de usar não é parte do requerimento. E nessa categoria se encontram linguagens como Brainfuck, que é uma linguagem Turing Complete, e teoricamente consegue fazer tudo. Só vai ser terrivelmente traumático tentar. Se você nunca viu, eis um exemplo de um programa que calcula números de fibonacci em Brainfuck. Sim, acreditem ou não, isso é uma linguagem de verdade, e é Turing Complete. Divirtam-se.</p>
<p>Por isso eu repito, sim, qualquer linguagem Turing Complete consegue fazer tudo que outra Turing Complete faz, mas não quer dizer que vai ser eficiente. Uma que confunde muita gente são Expressões Regulares que toda linguagem decente suporta hoje em dia. E não, Expressões Regulares não são Turing Complete. Primeiro que ele só se move numa direção. Quando adicionaram backreferences não lembro se passava a ser Turing Complete, mas pra todos os propósitos pode considerar que não é. Como o próprio nome diz, ela faz parte da categoria de Linguagens Regulares, que são linguagens que podem ser definidas puramente com Expressões Regulares. Você vai entender isso se estudar autômato finito determinístico. Se estudar compiladores vai ver isso e chegar em coisas como a hierarquia de Chomsky. Fica de lição de casa também.</p>
<p>Outros exemplos que você já deve ter ouvido falar e é verdade, SQL não é Turing Complete e HTML ou XML não são Turing Complete. Na prática sim, podem ser consideradas linguagens de programação, mas não são completas, ou seja, não conseguem fazer o que outras linguagens conseguem. Pra um engenheiro isso não importa muito, o que importa é a eficiência pra certos casos de uso. Por exemplo, o horroroso Brainfuck é Turing Complete, mas é inútil pra ser usado de verdade a menos que você seja sadomasoquista. Já HTML, mesmo não sendo Turing Complete, é beeem mais útil na prática. Então é uma discussão mais acadêmica que não tem tanta utilidade a menos que, de novo, você seja alguém trabalhando na pesquisa e desenvolvimento de novas linguagens por exemplo.</p>
<p>Finalmente, se for pra ser rigorosamente formal, nenhum computador nem linguagem é Turing Complete de verdade porque não temos memória infinita. Turing Complete é o ideal inatingível, o topo da cadeia alimentar da ciência da computação. Nada que foi construído até hoje supera o que é possível fazer com uma máquina de Turing teórico, a gente só se aproxima. Mesmo se pensarmos em um computador quântico, ele também não consegue fazer nada a mais que uma máquina de Turing já não faça. Vai ser absurdamente eficiente, mas só isso: eficiente. É importante esse conceito pra entender que o paradigma geral de computação que conhecemos hoje foi delimitado por Alan Turing e ninguém conseguiu pensar em outro modelo que supera o dele. Não é impossível, só ninguém conseguiu ainda.</p>
<p>Agora, voltando sobre a diferença entre grandes máquinas de calcular e um computador moderno, ou seja, uma máquina universal de Turing, agora você já sabe que muita gente chama as máquinas de Babbage, o de Zuze, o Eniac, o Colossus como &ldquo;computadores&rdquo;. Mas na real são máquinas de calcular glorificadas. Não são máquinas de Turing e portanto não tem parentesco com nossos computadores modernos. Se eu usar a analogia de parentesco, o computador de Babbage tem tanto parentesco com nossos PCs ou Smartphones quanto um macaco tem parentesco com seres humanos. O DNA bate uma porcentagem alta, mas nós e os macacos somos espécies completamente diferentes. Nós não descendemos dos macacos.</p>
<p>Apesar do Turing ter desenvolvido toda a matemática que dá origem à ciência da computação; a máquina de Turing é teórica, um modelo. Quem de verdade trabalhou no desenvolvimento do primeiro hardware que pode ser considerado de fato o primeiro computador moderno completo	 de verdade foi John Von Neumann no projeto do EDVAC, cujo paper pode ser considerado as bases do computador moderno. Por isso chamamos a arquitetura de hardware de todo computador como arquitetura Von Neumann. E segundo ele, o que o Turing desenvolveu é impressionante, mas ele não chegou a descrever como de fato construir uma máquina de Turing de verdade. Aí o Von Neumann preencheu as lacunas que faltavam do ponto de vista da engenharia do hardware.</p>
<p>Eu falei bastante de Turing e todo mundo hoje em dia já ouviu falar dele, especialmente depois do filme e por outros motivos que não tem nada a ver com ciências da computação. Mas eu acho que a maioria dos programadores não ouve falar em Von Neumann como deveria e eu vou corrigir isso hoje. E essa descrição vai ser lotada de superlativos, pode se preparar. Eu gostei de um artigo que saiu no blog Cantors Paradise em 2019 escrito por Jørgen Veisdal e vou pegar emprestado alguns trechos, mas recomendo ler o artigo completo que vou deixar nas descrições abaixo.</p>
<p>O artigo abre com uma frase sem autoria que diz &ldquo;A maioria dos matemáticos prova o que eles podem, Von Neumann prova o que ele quiser&rdquo;. Pois é, eu falei que ia ter superlativos. E o artigo continua descrevendo como &ldquo;o último representante dos grandes matemáticos&rdquo;. Qualquer um poderia descrevê-lo como a pessoa mais inteligente que já viveu e não seria um exagero. Digamos que todo prêmio Nobel de ciências do século XX iria reconhecer Von Neumann não como um par, mas muitas vezes como um superior. E não seria exagero dizer que se a física teórica teve Albert Einstein, a matemática e computação não ficaram nem um pouco longe, porque tiveram Von Neumann.</p>
<p>Ele nasceu no começo do século XX em Budapeste, na Hungria e desde criança já era prodígio. Histórias circulam que quando ele tinha seis anos já conseguia fazer cálculos tipo dividir números de 8 dígitos de cabeça e conversar em grego antigo. Isso não pareceu grande coisa? Ele já era proficiente em cálculo aos 8 anos e leu a teoria das funções de Emile Borel aos 12. E segundo relatos de muitos que o conheceram ao longo dos anos, todos dizem que ele tinha memória eidética. Lembram do Sheldon de The Big Bang Theory? Pois é, só que aqui era real. Ele era capaz de repetir tudo que havia lido, mesmo anos depois.</p>
<p>Num dos relatos no livro The Computer from Pascal to Von Neumann de Herman Goldstein, ele diz o seguinte: &ldquo;uma de suas habilidades impressionantes era seu poder de lembrança absoluta. Dentro do que eu posso dizer, Von Neumann era capaz de ler um livro ou um artigo e citar de volta palavra por palavra; e ele podia fazer isso anos depois sem hesitar. Ele também conseguia traduzir sem diminuir a velocidade da sua língua original pra inglês. Numa ocasião eu testei sua habilidade e pedi que ele me dissesse como o conto clássico A Tale of Two Cities começava. E assim ele começou a recitar o primeiro capítulo inteiro, sem pausas, e continuou citando até que eu pedi que ele parasse depois de uns dez ou quinze minutos&rdquo;.</p>
<p>Puta que o pariu. Esse é o tipo de coisa que é quase super poder de herói da Marvel. O cara era tipo um Reed Richards com Tony Stark. Mas isso é só a ponta do iceberg. Quando von Neumann entrou pra faculdade em 1921 ele já tinha escrito um paper com seu tutor sobre uma generalização do teorema de Fejér. Aos 18 anos ele já era reconhecido como um matemático propriamente dito. Colaborou em papers sobre teoria dos conjuntos em Berlin e teve aulas de física, incluindo estatística mecânica com Albert Einstein. Ah sim, ele tava na dúvida de que faculdade fazer, então eventualmente se formou tanto em engenharia química quanto Ph.D em matemática. Aos 24 anos. Sim, ele fez duas faculdades ao mesmo tempo e se formou nas duas  summa cum laude, ou seja, com a maior das honras.</p>
<p>Um dos professores de matemática relatou o seguinte: &ldquo;Durante um seminário pra estudantes avançados em Zürich que eu tava ensinando, eu cheguei num certo teorema, e eu disse que ele ainda não estava provado e que poderia ser difícil. Von Neumann não disse nada por cinco minutos, daí levantou a mão. Quando eu chamei ele pra ir pra lousa, ele foi e escreveu a prova. Depois disso eu tive medo de Von Neumann.&rdquo; Ah sim, e esse professor era George Pólya, por si só um dos maiores matemáticos da Hungria.</p>
<p>Depois disso ele continuou trabalhando em pesquisa com o maior matemático do mundo na época, David Hilbert. E durante esse período ele escreveu um monte de papers sobre teoria dos conjuntos e lógica enquanto ainda estava na faixa dos 20 anos. E em resumo, sua maior contribuição à teoria dos conjuntos é o que seria chamado de teoria de Von Neumann-Bernays-Gödel. E não, eu não sei o que isso significa. Fica de lição de casa pra vocês.</p>
<p>Mais ou menos na mesma época que estava contribuindo pra teoria dos conjuntos, ele também provou um teorema conhecido como minimax pra jogos de soma zero. E isso criaria a fundação pro novo campo de estudos que viria a ser chamado de teoria dos jogos. Lembra aquele filme do Russell Crowe, Mente Brilhante? Por causa do filme todo mundo acha que foi John Nash que deu origem à teoria dos jogos, e apesar de ter sido importante depois, quem criou a fundação toda foi Von Neumann. Em colaboração com o economista Oskar Morgenstern, Von Neumann depois publicaria o livro definitivo em jogos cooperativos de soma zero, intitulado Teoria dos Jogos e Comportamento Econômico em 1944.</p>
<p>Mas voltando pra 1929, ou seja, quando tava com 26 anos ele já tinha publicado 32 papers de pesquisa, quase um paper por mês. E até aqui ele já influenciou os campos de teoria dos conjuntos e ajudou a fundar teoria dos jogos. O que mais? Nessa época um tema que tava só começando gerava discussões ferrenhas entre físicos, a tal da física quântica. Numa dessas disputas estava nomes que você já ouviu falar, Heisenberg e Schrödinger. E não, não é esse Heisenberg e sim este outro. Schrodinger era meio hater do Heisenberg, dizendo que as formulações dele tava tudo errado.</p>
<p>Iniciando com um incidente entre esses dois, nos anos seguintes o von Neumann publicou vários papers que estabeleceram um framework matemático rigoroso pra mecânica quântica, hoje conhecido como axiomas de Dirac-Von Neumann, isso sem contar que ele explicou como entender mecânica quântica do ponto de vista estatístico. Ele teve um insight que nem Heisenberg, nem Niels Bohr, ou Schrodinger tiveram, baseado no campo de estudos de seu mentor David Hilbert, os espaços vetoriais de Hilbert que eu menciono no meu video sobre Supremacia Quântica. Mas só resumindo que enquanto gênios como Schrodinger tavam batendo cabeça entre eles, Von Neumann foi e encerrou a discussão.</p>
<p>Só que daí Hitler subiu ao poder na Alemanha, a guerra tava pra começar e Von Neumann foi pros Estados Unidos, recrutado pra Universidade Princeton como professor visitante. Logo no começo ele se virou pra teoria ergódica. E sim, eu também não tenho a mínima idéia do que diabos é isso. Mas segundo o artigo, teoria ergódica é uma área da matemática que estuda as propriedades estatísticas de sistemas dinâmicos determinísticos. Enfim, basta dizer que pra surpresa de ninguém a essa altura, Von Neumann fez contribuições essenciais incluindo o teorema ergódico médio de Von Neumann, considerado a primeira base matemática rigorosa pra estatística da mecânica de líquidos e gases. O matemático Paul Halmos disse sobre isso: &ldquo;se von Neumann nunca tivesse feito mais nada, só isso já teria sido suficiente pra garantir imortalidade na matemática&rdquo;.</p>
<p>Com a Segunda Guerra iminente, os nazistas perseguindo judeus, era questão de tempo pra chegarem na Universidade de Göttingen na Alemanha, que era então a referência do mundo em matemática e física. Mas por causa da perseguição, diversos nomes famosos ou foram expulsos ou fugiram e muitos foram pro recém formado Instituto pra Estudos Avançados nos Estados Unidos. Além de Von Neumann e Wigner veio Einstein que era judeu, claro. Daí outros como Max Born, Leó Szilárd, Edward Teller, Edmund Landau, James Franck, Richard Courant e vários outros foram também.</p>
<p>Enquanto estava no Instituto de Estudos Avançados, Von Neumann, só pra variar, fundou o campo de geometria contínua. Assim como no caso da teoria ergódica, ele publicou dois papers, um provando sua existência e discutindo suas propriedades, e outra dando exemplos. Fundar campos novos virou rotina pro Von Neumann. Fico imaginando ele acordando e pensando “hmm, que novos campos de estudo que não existem eu quero revolucionar hoje?” E pra provar isso, lá pro meio dos anos 30 ele desenvolveu expertise também na ciência de explosões, fenômeno que é bem difícil de modelar matematicamente. Veja uma explosão em CG em filmes dos últimos 20 anos e você vai ver que só hoje conseguimos simular explosões de forma mais realista.</p>
<p>De qualquer forma Von Neumann acabou se tornando uma autoridade também na matemática de cargas moldadas, cargas explosivas pra focar o efeito da energia na explosão. E sua maior contribuição pra bomba atômica foi no conceito e design das lentes explosivas que era necessárias pra comprimir o núcleo de plutônio da arma Fat Man, que foi a usada em Nagasaki. Ah sim, ele participou do projeto Manhattan e era conhecido de Richard Feynman e Robert Oppenheimer.</p>
<p>No currículo de Von Neumann até agora temos contribuições na matemática, incluindo teoria dos conjuntos, teoria dos números e espaços de Hilbert; na física, em especial em mecânica quântica; na economia com a teoria dos jogos; na biologia com autômatos celulares e depois em computadores e inteligência artificial. E aqui voltamos no nosso tópico central.</p>
<p>Seu trabalho com computadores começou em Princeton no meio dos anos 30 depois de se encontrar com um jovem Alan Turing de 24 anos, quando ele passou um ano no Instituto de Estudos Avançados entre 1936 e 37. Turing começou sua carreira trabalhando no mesmo campo que Von Neumann, em teoria dos conjuntos, lógica e o problema de decisão de Hilbert, o tal de “Entscheidungsproblem” e por isso seu paper seminal de 1936 se chama “On Computable Numbers, with an Application to the Entscheidungsproblem”.</p>
<p>Quando terminou seu ph.D em 1938, Turing já tinha estendido o trabalho de Von Neumann e Gödel e introduziu lógica original e a noção de computação relativa, aumentando suas máquinas de Turing anteriores por máquinas Oráculo, permitindo o estudo de problemas que vão além das capacidades da máquina de Turing original. Von Neumann chegou a convidar Turing pra uma posição de pós-doutorado como assistente de pesquisa depois do ph.D mas Turing rejeitou e em vez disso viajou de volta pra Inglaterra, pra participar do filme do Cumberbatch quebrando o código dos nazistas e ajudando a vencer a guerra.</p>
<p>Apesar do Turing ter ido embora, Von Neumann continuou pensando sobre computadores durante o fim dos anos 30 e durante a guerra. Depois da sua experiência no projeto Manhattan ele foi atraído pelo projeto do ENIAC da universidade da Pensilvânia durante o verão de 1944. E ele tinha boa noção da quantidade massiva de cálculos necessários pra prever o raio de uma explosão nuclear, ou o planejamento do caminho da bomba e também sobre como quebrar  esquemas de criptografia. São centenas de cálculos complexos diferente e lembrem que o ENIAC era uma calculadora eletrônica gloficada e horrorosa, que só conseguia fazer uma tarefa de cada vez e toda vez pra reprogramar precisava reconfigurar dezenas e dezenas de cabos.</p>
<p>Pro Von Neumann tava claro que isso não ia rolar e em 1945, pro projeto da construção do computador EDVAC, ele propôs a descrição pra uma arquitetura de computadores hoje conhecido como arquitetura Von Neumann, que inclui o básico de um moderno computador eletrônico digital, que inclui: uma unidade de processamento que contém uma unidade de lógica aritmética e registradores de processo. Uma unidade de controle que contenha um registrador de instrução e um contador de programa. Uma unidade de memória que possa armazenar dados e instruções. Armazenamento externo e mecanismos de entrada e saída. Ou seja, ele especificou o que hoje chamamos de CPU, RAM e tudo mais. É a descrição de um computador moderno.</p>
<p>Como eu disse antes, o grande insight que separa o computador moderno das grandes calculadores, que tanto Turing quanto Von Neumann pensaram mas que não era nem um pouco óbvio na época, era o conceito de instruções de programa e dados conviverem no mesmo espaço de memória e armazenamento, em vez de serem duas coisas separadas como na máquina de Babbage. E não contente em ter feito só isso, claro que Von Neumann foi além.</p>
<p>Se você estudou algoritmos e estruturas de dados na faculdade e viu algoritmos de ordenação, com certeza esbarrou no Merge Sort, que divide arrays pela metade antes de ordená-los recursivamente, pra no final fazer o merge do resultado. Foi inventado por Von Neumann, claro. E ele mesmo escreveu o algoritmo pro computador EDVAC. De bônus, já que parece que ele tinha tempo sobrando, ele introduziu computação estocástica, embora a idéia era tão inovadora pra época que não seria implementada por mais uma década. E por fim, ele criou o campo de autômatos celulares, que precedeu a descoberta da estrutura do DNA por muitos anos.</p>
<p>E esse provavelmente vai ser o maior, melhor e mais influente currículo de um acadêmico e engenheiro que você jamais vai ver. E só pra jogar mais lenha na fogueira. Você poderia imaginar que um matemático do calibre de Von Neumann, que tá ali pau a pau no grupo de nomes como Einstein ou Schroedinger ou Hilbert, deve ser um daqueles matemáticos introspectivos, quietos, tímidos, isolados e anti-sociais bem no estilo Turing do Cumberbatch, né?</p>
<p>Não, pelo contrário, segundo relatos de conhecidos: &ldquo;Festas e vida noturna tinham um apelo especial pra Von Neumann. Enquanto ensinava na Alemanha, ele foi assíduo da era de cabarés do circuito noturno de Berlin. E as festas na casa de Von Neumann eram frequentes, e famosas, e longas.&rdquo; Ele era aquele tipo mais bonachão, que fazia piadinhas infames. E durante o período no Instituto de Estudos Avançados ele recebia reclamações porque ligava seu gramofone super alto tocando marchas alemãs no escritório, distraindo os escritórios vizinhos, incluindo Albert Einstein, que ficava putaço.</p>
<p>Von Neumann morreu de câncer em 1957, aos 53 anos. Ou seja, tudo isso que eu contei, foi antes dos 50 anos. Se você é do tipo que gosta de ter alguém em quem se espelhar, um ideal inatingível, esquece porcaria de CEO de startup ou influencier mamão com açúcar. Isso é a definição de padrões muito baixos. Se é pra sonhar, então sonha alto e coloca um Von Neumann como objetivo de vida pra ser fã. Quando estiver em dúvida, pense, o que Von Neumann faria? Hmm, ok, vamos pegar um problema que todo mundo acha impossível resolver, e resolver.</p>
<p>E por hoje é isso aí! Eu disse que essa história ia ser comprida, mas eu acho que a grande maioria dos programadores simplesmente desconhece John Von Neumann. Todo mundo já ouviu falar de Alan Turing mas só o que tem no filme. E muito programador iniciante adora falar sobre Turing Complete e se eu perguntar o que é uma máquina de Turing só sobra silêncio.</p>
<p>Turing e Von Neumann definiram o que é o computador moderno que usamos até hoje, tanto do ponto de vista do rigor matemático até a arquitetura do hardware propriamente dito. E por isso eu considero essa dobradinha como os verdadeiros pais da computação. E eu sei, muita gente vai perguntar, mas cadê o doutor Alonzo Church e Lambda Calculus? Relaxa que tá nos planos falar disso uma hora. Pra não perder quando sair, assine o canal e clique no sininho pra ser notificado. Se curtiu o video deixa um joinha e compartilhe o video com seus amigos. A gente se vê, até mais!</p>
]]></content:encoded><category>ciencia-da-computacao</category><category>historia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #80 - O Guia +Hardcore de Introdução à COMPUTAÇÃO</title><link>https://akitaonrails.github.io/2020/06/04/akitando-80-o-guia-hardcore-de-introducao-a-computacao/</link><guid isPermaLink="true">https://akitaonrails.github.io/2020/06/04/akitando-80-o-guia-hardcore-de-introducao-a-computacao/</guid><pubDate>Thu, 04 Jun 2020 09:44:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/8G80nuEyDN4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIÇÃO&lt;span class="hx:absolute hx:-mt-20" id="descrição"&gt;&lt;/span&gt;
&lt;a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Errata:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;em 53:46 eu falo &amp;ldquo;1 1&amp;rdquo; mas era &amp;ldquo;aa&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Se tem um tema que eu queria fazer desde que abri o canal é apresentar algumas das fundações da computação, tanto do ponto de vista de história e da evolução, mas principalmente dos fundamentos básicos que eram vários 60 anos atrás e ainda são válidos até hoje.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/8G80nuEyDN4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIÇÃO<span class="hx:absolute hx:-mt-20" id="descrição"></span>
    <a href="#descri%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Errata:</p>
<ul>
<li>em 53:46 eu falo &ldquo;1 1&rdquo; mas era &ldquo;aa&rdquo;</li>
</ul>
<p>Se tem um tema que eu queria fazer desde que abri o canal é apresentar algumas das fundações da computação, tanto do ponto de vista de história e da evolução, mas principalmente dos fundamentos básicos que eram vários 60 anos atrás e ainda são válidos até hoje.</p>
<p>O video de hoje vai abrir com um tema de videogames, mas vamos descer bem fundo no cérebro do Nintendinho pra criar um vocabulário que eu vou usar na Parte 2, quando de fato vou mexer num emulador de Nintendo. E esse vocabulário vai servir não só pra videos futuros meus como pra tudo que você for fazer em computação. Esta é a base, sem esta base tudo que você tentar aprender de avançado depois vai ser mais difícil.</p>
<p>Infelizmente é muito difícil empacotar estes temas de uma forma que não seja maçante. Espero ter conseguido pelo menos interessar vocês no assunto.</p>
<p>Canais:</p>
<ul>
<li>Ben Eater: <a href="https://www.youtube.com/user/eaterbc"target="_blank" rel="noopener">https://www.youtube.com/user/eaterbc</a></li>
<li>Retro Game Mechanics Explained: <a href="https://www.youtube.com/channel/UCwRq"target="_blank" rel="noopener">https://www.youtube.com/channel/UCwRq</a>&hellip;</li>
</ul>
<p>Playlist:</p>
<ul>
<li>Build a 65c02-based computer from scratch: <a href="https://www.youtube.com/watch?v=LnzuM"target="_blank" rel="noopener">https://www.youtube.com/watch?v=LnzuM</a>&hellip;</li>
<li>Building an 8-bit breadboard computer!: <a href="https://www.youtube.com/watch?v=Hyznr"target="_blank" rel="noopener">https://www.youtube.com/watch?v=Hyznr</a>&hellip;</li>
</ul>
<p>Links:</p>
<ul>
<li>Ben Eater “Build a 6502 Computer” Kit: <a href="https://eater.net/6502"target="_blank" rel="noopener">https://eater.net/6502</a></li>
<li>VASM A portable and retargetable assembler: <a href="http://sun.hasenbraten.de/vasm/"target="_blank" rel="noopener">http://sun.hasenbraten.de/vasm/</a></li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Se você está cético sobre este episódio porque não tem nenhum interesse por videogames, não se preocupe, a intenção de hoje é apresentar muitos dos conceitos fundamentais pra você entender melhor o que é programação. Este vai ser mais um daqueles episódios que você vai ficar se perguntando &ldquo;mas onde diabos esse japonês quer chegar&rdquo; mas com paciência as peças se encaixam. E já aviso que esse também vai ser um vídeo bem complexo. Pausa, volta e assiste de novo. Não é um conteúdo que dá pra entender 100% assistindo só uma vez.</p>
<p>Com tudo isso fora do caminho vamos lá. Quem me acompanha nos Instagram da vida sabe que um dos meus hobbies favoritos é jogar videogames. Além dos jogos Triple A eu também gosto bastante de voltar aos jogos da minha infância, de Alex Kidd a Sonic a Super Mario. Eu não faço coleção de cartuchos porque não teria espaço físico pra tudo. São milhares e milhares de jogos. Felizmente hoje é muito simples. Basta baixar uma das dezenas de emuladores e carregar a ROM dos games. Por exemplo, posso carregar este Retro Arch que tenho no meu desktop e boom, Super Mario.</p>
<p>A idéia deste video veio quando eu tava assistindo dois dos meus canais de YouTube favoritos. O primeiro é o Retro Game Mechanics Explained. O cara é muito bom. Por exemplo, pra quem gosta do Pokemon original de GameBoy, ele explica bit a bit o mistério do elusivo MISSINGNO, recomendo que assistam. Mas o video que eu tava assistindo me relembrou de outro dispositivo que existia durante os anos 90.</p>
<p>Muitos de nós gostamos de games só que mais como passatempo, e não somos tão obsessivos em completar 100% dos jogos ou fazer speedruns pra bater o recorde de quem acaba no menor tempo, e só porque um jogo é antigo não quer dizer que é fácil. Muitos tinham dificuldade maior e mais brutal que jogos da atualidade. Tentem jogar coisas como o Megaman original ou Battletoads do Nintendinho pra ver o que quero dizer. Fora que antigamente nem todo jogo tinha uma bateria pra salvar o progresso, então se dava game over você realmente tinha que recomeçar do zero.</p>
<p>Então surgiram dispositivos que possibilitavam &ldquo;cheats&rdquo;. Quem joga online conhece essa palavra e os diversos software anti-cheat pra evitar que jogadores trapaceiem com hacks. Anti-cheats que vira e mexe criam polêmica, como no recente caso do jogo Valorant, cujo software anti-cheat foi acusado de causar até estragos de hardware.</p>
<p>Enfim, nos anos 90 a tecnologia era mais simples. Existiam diversas marcas diferentes como Action Replay, Game Shark ou o mais famoso, o Game Genie. Funcionava assim, você plugava o cartucho na parte de cima e daí plugava o conjunto no console. O Game Genie ficava entre o cartucho e o console. Ele engana o console. Por exemplo, o console pergunta pro cartucho, com quantas vidas que o Super Mario tem que começar? Daí tá gravado em algum lugar no cartucho o número 3, mas aí se você colocar um código mágico no Game Genie, como AATOZE, ele devolve 9 pro console no lugar de 3 e o jogo começa com 9 vidas.</p>
<p>Eu vou explicar o game genie em mais detalhes na Parte 2 deste video, de uma forma complementar que o Retro Game Mechanics explica. Mas antes de chegar nisso vou fazer uma longa tangente hoje com meu segundo canal favorito, o Ben Eater. O canal do Ben é hardcore, ele explica todas as fundações eletrônicas da computação, em particular exatamente a arquitetura por trás de computadores como o nintendinho. Eu vou usar muito material do canal dele e condensar num único video pra tentar fazer vocês finalmente enxergarem a mulher de vermelho no código do Matrix.</p>
<p>(intro)</p>
<p>O nintendinho roda o famoso processador MOS 6502 de 8-bits, derivado do desenho do Motorola 6800 e fabricado pela MOS Technologies a partir de 1975. Esse processador foi usado não só no nintendinho mas em dezenas de outros consoles e computadores pessoais dos anos 80 incluindo coisas como o lendário Apple II, o avô dos Macintosh, ou o concorrente Atari, e os britânicos Commodore e BBC Micro. Ele foi um dos componentes que ajudou a iniciar a revolução dos computadores pessoais dos anos 80. Exatamente por isso é um bom ponto de partida pra entender a computação de hoje em dia.</p>
<p>Apesar de internamente processar em 8-bits, ele possui uma interface de comunicação de 16-bits. 16-bits em hexadecimal, que são 2 bytes, se representa com 4 caracteres. Já um valor de 8-bits, ou 1 byte, se representa com dois caracteres. Como o código mágico AATOZE que falei tem 6 caracteres então já começa a ficar fácil de entender o que está acontecendo aqui. Muito provavelmente é um endereço de 16-bits e um valor de 8-bits, exatamente o que o 6502 entende. E de fato esse código se transforma no endereço 906A e no valor 09, que dá as 9 vidas ao Super Mario.</p>
<p>É importante que vocês entendam como enxergar binários e hexadecimais. Se você já sabe disso, pode pular pra este tempo aqui em baixo. De qualquer forma, tudo em computação é binário. Este vídeo que você tá assistindo é uma stream de bits. O jogo do Super Mario é um punhado de bits. Um arquivo de texto ou código em Javascript é um arquivo de bits. Muita gente acha que arquivos texto e binário são diferentes. Mas não, ambos são binários. A diferença é como você representa esse binário depois de abrir, pra mostrar pro usuário.</p>
<p>Deixa eu mostrar. Na maioria dos sistemas operacionais existem editores de texto, seja o miserável Notepad no Windows ou o moderno VS Code. Se eu abrir um arquivo que contém texto, qualquer um vai abrir sem nenhum problema. Mas se no mesmo editor eu tentar abrir o arquivo da ROM do Super Mario, vai aparecer esse monte de lixo na tela. Isso porque um editor de textos só sabe interpretar um sub-conjunto de binários que, por convensão, representa letras, números e símbolos.</p>
<p>Se eu abrir um editor e escrever a palavra &ldquo;Hello World&rdquo; e gravar, podemos abrir o binário cru com um visualizador de binário, como o comando <code>xxd -b</code> e pronto, isto é o que tá gravado no arquivo. Veja este primeiro bloco de 8 bits é a letra &ldquo;H&rdquo;, vejam estes dois blocos repetidos, são os dois &ldquo;L&rdquo; e assim por diante. Claro que ler bit a bit assim é bem inconveniente, então podemos pedir pra ferramenta converter esses bits em uma representação hexadecimal, rodando o mesmo comando xxd sem o parâmetro -b temos os seguite.</p>
<p>Agora sim, aquele bloco de bits é o número 48 em hexadecimal, que não é a mesma coisa que 48 em decimal. Já vou explicar. Essa é a letra &ldquo;H&rdquo;. Depois temos 6c e 6c que obviamente é o código hexadecimal pra letra &ldquo;L&rdquo;. E assim por diante. Quem disse que 48 é H ou que 6c é L? É uma convenção internacional conhecida como Unicode. É um conjunto de tabelas e regras que mapeiam cada caracter em todas as línguas num código de 1 ou mais bytes. Emojis também, cada um tem seu código. Tudo é número. A tabela mais antiga e mais simples que ainda acabamos usando é a ASCII.</p>
<p>Pra quem não sabe, o que chamamos de &ldquo;ROM&rdquo; em retro gaming são os bits que vem gravados num cartucho e que extraimos pra dentro de um arquivo, é isso que um emulador de nintendinho lê no lugar do cartucho de verdade. Com o mesmo comando <code>xxd</code> mas passando com um pipe pro <code>more</code> pra podermos ver as primeiras linhas, temos o seguinte. Deixa eu explicar melhor o que é essa representação do comando xxd. Aliás, existem diversas ferramentas que conseguem abrir binários, um dos mais usados é o <code>hexdump</code> mas prefiro mostrar o xxd pra dar outra alternativa. A diferença dos dois é que o hexdump só mostra o binário na forma de hexadecimal, o xxd faz a mesma coisa mas consegue também gravar de volta pro binário. Por isso se somar o xxd com o vim você consegue editar qualquer binário diretamente.</p>
<p>Bom, a coluna da direita são os endereços, seguido de uma linha de 16-bytes organizados em grupos de 4 letras, cada um com 2 bytes, num total de 8 blocos de 4 letras. Cada 1 letra é um byte em hexadecimal. Você começa no endereço 0 e vai contando até 15 que é a letra F, dai na segunda linha o endereço começa em 16 que é o hexadecimal 10. Conta mais 16 elementos, agora chega no elemento 32 que em hexa é 20 e assim por diante.</p>
<p>Na 3a coluna ele mostra &ldquo;.&rdquo; (ponto) quando o byte não tem representação em texto. Quando tem, ele mostra o que seria o código na tabela ASCII. Vocês vão notar que nos primeiros 3 bytes temos aparecendo o texto &ldquo;NES&rdquo; nessa terceira coluna. NES é como o Nintendinho é chamado nos Estados Unidos, o Nintendo Entertainment System. Obviamente não é coincidência, todo cartucho distribuídos nos Estados Unidos começa com esse código.</p>
<p>Isso é só uma etiqueta arbitrária pra facilitar identificar o que é esse binário. Muitos outros formatos de arquivo fazem a mesma coisa. Se eu abrir uma imagem formato PNG com o xxd olha só, aparece um código 89 e depois os códigos ASCII que forma a palavra PNG. Se abrirmos uma imagem JPEG todos começam com FF D8 FF E0 e alguns bytes pra direita aparece JFIF que é sigla pra JPEG File Interchange Format. Se listar um binário compilado com extensão .class de Java olha o que aparece. O identificador de todo binário Java é o hexadecimal que forma as palavras &ldquo;cafe babe&rdquo;.</p>
<p>Se não ficou claro, o que estamos vendo na tela com o xxd não é o binário de verdade, assim como um editor de textos como Notepad mostra uma representação em forma de texto legível dos números binários reais, o xxd mostra os binários de uma forma que seja fácil da gente conseguir acompanhar. Essas colunas da esquerda e direita não existem. E não existe quebra de linha. Um binário são os bits escritos um atrás do outro numa única linha contínua até o fim. Pense num arquivo binário como um rolo de fita. Entendem agora porque você vê computadores antigos gravando dados num rolo de fita? Ou num cassette? É assim mesmo que funciona até hoje. Se você já programa em alguma coisa, pense num binariozão com um Array.</p>
<p>Existem vários sites e videos que ensinam em detalhes como converte e faz contas em binário e hexadecimal então vou resumir hiper resumido. Basicamente sistema decimal representamos números que vão de 0 a 9. Em hexadecimal representamos números indo de 0 a F, onde A é 10, B é 11, até F que é 15. Todas as regras aritméticas de soma, subtração etc funcionam igual. Quando você faz F + 1, coloca zero e sobe 1, daí o hexadecimal 1 0 equivale a 16. Sim, você precisa se acostumar a manipular números em hexadecimal. Até mesmo quando trabalha com front-end e CSS sabe que as cores são mapeadas em números hexadecimais e você faz conta com elas pra fazer degradê de cores por exemplo, você subtrai pra tornar todas as cores mais escuras ou faz soma pra elas ficarem mais claras.</p>
<p>Voltando pro cabeçalho do Java, o primeiro &ldquo;C&rdquo; não é a letra do alfabeto &ldquo;C&rdquo; e sim o número 12 em hexadecimal. Já brincaram com calculadora de cabeça pra baixo? Por exemplo, você digita 07734 e vira a calculadora e temos &ldquo;HELLO&rdquo; ou digitamos 376006 e de cabeça pra baixo temos Google. Todo mundo já brincou disso e o CAFE BABE do java é parecido só que como em hexadecimal temos de verdade as 6 primeiras letras do alfabeto, dá pra representar palavras. Mas é só uma brincadeira mesmo.</p>
<p>Agora, voltando ao binário. Computar em bits tem algumas vantagens pro computador fazer alguns tipos de conta. Em particular multiplicar por 2 e dividir por 2. Pegue o número 4 que em 4-bits seria 0 1 0 0. Se eu mover 1 pra esquerda vira 1 0 0 0 que é 8. E pra dividir? Oposto, só mover tudo pra direita então o 4 que é 0 1 0 0 vira 0 0 1 0 que é 2. Estou simplificando, claro, porque se dividir um número ímpar, eu não vou ter o resto assim. Aliás, isso que eu falei de mover pra esquerda ou direita é o que se chama de operador bitwise em qualquer linguagem moderna, o shift left que é &laquo; e shift right que é &raquo;.</p>
<p>Se abrir qualquer navegador e inspecionar um elemento, você abre o tal console de Javascript. E aqui podemos exercitar o que eu acabei de falar. Por exemplo, se eu quiser representar um número em binário começo digitando 0b daí por exemplo 0 1 0 0 que é 4. E é exatamente isso que ele mostra. Vou criar um texto chamado &ldquo;palavra&rdquo; e dentro colocar &ldquo;hello world&rdquo; que tem 11 letras. Se eu quiser pegar a décima primeira letra dessa palavra posso fazer <code>palavra[10]</code> e 10 porque em programação tudo começa de 0. Então 0 é &lsquo;h&rsquo;, 1 é &rsquo;e&rsquo; etc e a posição 10 é a letra &ldquo;d&rdquo;.</p>
<p>Podemos chegar na mesma letra se fizermos <code>palavra[0x0A]</code> lembram que eu falei que A em hexa é 10? Pro Javascript saber que isso na verdade é um número colocamos 0 x antes. Pra ficar mais hardcore podemos fazer <code>palavra[0b1010]</code> e esse 0 b é pro Javascript saber que é um número binário. Tanto o 0xA quanto 0b1010 são formas diferentes de escrever o mesmo número 10. É tudo o mesmo número, só que podemos representar de formas diferentes. Internamente sempre é tudo um número binário. As linguagens de programação só facilitam e nos deixam usar números em decimal ou mesmo letras.</p>
<p>Se eu fizer 10 shift right 1 é divisão por 2, e eu posso fazer <code>palavra[0xA &gt;&gt; 1]</code> onde A é 10 em hexa, shift right 1 que é divisão por 2 e ele devolve espaço que é o que tem na posição 5 naquele hello world.</p>
<p>Entendido isso, vamos pular pra outro conceito. Todo mundo ouve falar sobre computadores 32 bits ou 64 bits. Se você pensar em base decimal dá impressão que estamos só dobrando de 32 pra 64. O que isso significa? Em resumo significa que internamente o computador consegue fazer cálculos e processar números de 32 ou 64 bits de comprimento de uma só vez. Até agora eu fiz exemplos com números de 4 bits que dá pra contar só até 255. Lembra que eu falei que cada 1 bit que eu adiciono pra esquerda eu estou multiplicando o número todo por 2? Então 5 bits já é o dobro de 4 bits. 6 bits é o dobro de 5 bits. 33 é o dobro de 32 bits e 64 é 32 bits ao quadrado. A metade de 64-bits não é 32 e sim 63-bits.</p>
<p>Em 32 bits podemos representar até quase 4.3 bilhões. Em 64-bits podemos representar um número gigante 1.8 x 10 elevado a 19, ou seja, um numerozão de 19 casas. É a casa dos quintilhões. Entendam essas relações de dimensão. A vantagem de usar hexadecimal é que esse numerozão de 64-bits pode ser representado por 16 caracteres. Mesmo que você ainda esteja coçando a cabeça neste ponto, veja que não é coincidência que o numerozão de quintilhões de 64-bits fica bonitinho tudo com FFFFF&hellip; em hexa. Isso porque propositalmente usamos números derivados da base 2. Por isso um megabyte não é 1000 bytes e sim 1024 bytes. Em hexa seria 4 0 0. Tendemos sempre a usar números que são redondos em hexadecimal.</p>
<p>De qualquer forma 64-bits é um número difícil de visualizar na cabeça. Não se preocupe, eu acho que até o final do video vocês vão começar a se acostumar com isso. Mas eu acho que a forma mais fácil de entender como as coisas funcionam hoje é ver como funcionava ontem, porque o que temos hoje são mais ou menos camadas extras em cima da mesma base de antigamente. Componentes mais modernos, métodos de fabricação melhores, mas a fundação ainda é exatamente a mesma. Por isso quero voltar pros anos 70, mais especificamente pra 1975 e olhar mais de perto o cérebro do nosso nintendinho.</p>
<p>Se abrirmos o nintendinho, esta é a placa mãe com os diferentes chips. Mas por agora vamos nos focar neste aqui, o MOS 6502 que eu falei antes. Se eu quisesse mostrar o nível zero de verdade da fundação, precisaria começar com o básico em eletrônica. Mas eu também só sei o básico. É o que você aprenderia em Engenharia Elétrica ou Engenharia da Computação e não em Ciências. Mas eu quero resumir pelo menos, e pra isso resolvi pegar emprestado material de outro dos meus canais favoritos, do Ben Eater, que eu falei no começo do video.</p>
<p>Uma das playlists dele mostra como você constrói um processador de 8-bits do zero. E outra playlist dele, por acaso é como fazer um hello world com o 6502, o mesmo CPU do nintendinho. Só pra mostrar um Hello World ele levou meses produzindo uns 9 videos com mais de mais hora cada. São horas de video e eu vou resumir tudo em alguns minutos, então depois eu recomendo assistirem os videos dele com calma. Como eu disse daria pra descer no nível da física elétrica mas eu acho que a partir do nível que eu vou mostrar neste video é o mínimo suficiente pra se ter um desenho na cabeça pra conseguir visualizar o que um programa de fato está fazendo no hardware. Eu acho muito mais difícil aprender a programar sem entender o que acontece por baixo.</p>
<p>Quem está iniciando em eletrônica hoje em dia tem bastante material da hora pra treinar. Uma dessas ferramentas se chama breadboard, que é uma placa feita pra montar protótipos de circuitos eletrônicos sem precisar ficar soldando os componentes, você só encaixa os chips e fios na placa, liga a força, normalmente de 5V e pronto. Fazer circuitos integrados é literalmente programação de baixo nível. Ele inclusive vende kits com todos os componentes que você vai ver. Se tiver como investir, recomendo como bom aprendizado. É como se fosse brincar de Lego, mas bem mais interessante.</p>
<p>A lógica por trás de linguagens de programação modernas seguem os mesmos princípios da lógica em nível de hardware. Se você já ouviu falar isso de &ldquo;ter que saber lógica pra programar&rdquo; mas nunca entendeu onde começar, comece entendendo portas lógicas e máquinas de estado.</p>
<p><a href="https://www.youtube.com/watch?v=LnzuMJLZRdU"target="_blank" rel="noopener">https://www.youtube.com/watch?v=LnzuMJLZRdU</a> (part 1)</p>
<p>Pra começar veja o diagrama do 6502. Todo chip tem essas perninhas ou pinos, nelas, você liga ou desliga corrente em alguns pinos pra se comunicar com o chip, e os traços recebem corrente quando o chip quer enviar dados. Numa placa de verdade com solda e tudo, o que liga um pino de um chip a outro são traços condutores. Num breadboard se usa fios de metal mesmo pra facilitar. Agora vamos ver o diagrama de pinos, e a primeira coisa que quero chamar a atenção são esses pinos que vão de A0 até A15 e os pinos que vão de D0 até D7. Não é coincidência que são 16 pinos A e 8 pinos D.</p>
<p>Eu disse que o 6502 é um processador de 8-bits então quando você quer ler o resultado de algum processamento que ele fez, serão valores de 8 bits de comprimento, e eles vão trafegar por esses pinos D0 até D7. E quando o processador quer enviar um endereço pra buscar na memória RAM por exemplo, ele pode mandar um número de até 16-bits, que trafega por esses pinos de A0 até A15. Digamos que eu quero mandar o número em hexa <code>a9</code> pelos pinos de dados. Em binário seria 1 0 1 0 1 0 0 1, e cada um desses bits vai em um pino de D9 a D0.</p>
<p>Cada componente que coloca no Breadboard sempre vai ligar um pino de força e outro pino em ground ou Terra, é como você passa energia pro componente. Daí ele vai ligar outros pinos, por exemplo pra habilitar a CPU, outro pra conectar no clock. Um clock é exatamente o que o nome diz, é um componente com um cristal de quartzo que oscila, enviando um sinal elétrico numa frequência precisa e constante. Todo componente que processa alguma coisa no computador precisa estar ligado no mesmo clock pra tudo funcionar sincronizado. Um computador é uma grande orquestra de componentes e o clock é o maestro. Muitas instruções do CPU demoram mais de um clock, mas tudo é alinhado de acordo com a frequência desse clock. A menor unidade de processamento num computador é esse pulso que eu vou ficar chamando de clock durante o video.</p>
<p>Um clock de sistema convencional é pequeno como esse de 1 megahertz, ou 1 milhão de pulsos por segundo, que o Ben está mostrando, mas ele vai usar um mais feinho que ele mesmo construiu em outro video. A vantagem é operar mais devagar e dar a opção de poder pausar o clock e manualmente soltar um pulso de cada vez. todo componente do computador, incluindo a CPU, vai pausar junto e rodar um ciclo de clock de cada vez. Um processador 6502 original não suportava uma pausa dessas, mas esse que ele está usando é uma versão mais moderna que é fabricado até hoje e tem mais funcionalidades.</p>
<p>Continuando ele precisa ligar o pino que indica pro CPU rodar que é o RDY ou ready, depois o pino que liga com um botão de reset que é o RESB, daí liga o pino de clock e ligamos a placa do clock com força e terra na placa principal.</p>
<p>Só de fazer isso e ligando a placa principal numa tomada de 5V a CPU já tá funcionando e fazendo alguma coisa. Uma forma simples de monitorar pinos de dados é ligar LEDs direto em cada um deles, por exemplo ligando os pinos A0 a A4 em 5 LEDs pra gente ver se tem alguma coisa acontecendo. E de fato tem alguma coisa, mas bem aleatório.</p>
<p>Pra monitorar melhor, o Ben montou um sistema de monitoramento com Arduino em outro video e vai usar aqui. Um Arduino é um microcontrolador que tem algumas dezenas de pinos que servem tanto como entrada de dados ou saída. Então ele vai ligar primeiro os pinos de A0 a 15 da CPU com os pinos 22 a 52 no Arduino. O Arduino é programável, e podemos escrever programinhas simples e enviar pra ele executar. E é isso que o Ben vai fazer. Não se preocupe se você nunca viu um programa de Arduino, o código dessa demonstração é hiper simples, só prestar atenção que dá pra entender sem nenhum problema.</p>
<p>Primeiro vamos declarar uma lista com quais pinos do Arduino vamos trabalhar, e vai ser de 22 a 52.
Daí fazemos um loop lendo número a número e mandando um comando pra dizer que vamos ouvir o que vier nesse pino, ou seja, eles são INPUT
Agora fazemos outro loop pela mesma lista pra ir lendo o que vem em cada pino. Essa função devolve true ou false então quando vier true devolvemos o número 1 e quando vier false devolvemos 0 e escrevemos na tela
Daí escrevemos uma quebra de linha com print line e repete tudo de novo.
Finalmente não podemos esquecer de declarar a velocidade que vamos trafegar dados que vai se 57600 bauds, não se preocupe com esse número mágico.</p>
<p>Transmitimos o programa pro Arduino via essa interface, e abrimos o Monitorador de Sistema pra ver o resultado desse programa rodando. E vemos um monte de números binários, 16 bits no total que é o número de pinos que estamos monitorando. Entenda o que está acontecendo, o CPU está processando alguma coisa e setando bits nos pinos de endereço. Esses pinos estão ligados no Arduino que está lendo esses bits. Daí estamos num loop infinito puxando esses bits constantemente e imprimindo na tela. Mas não estamos ligados ao clock então ele vai ficar que nem doido lendo e imprimindo sem parar, sem qualquer ordem. Pra melhorar podemos sincronizar o Arduino ao mesmo clock também.</p>
<p>Eis um exemplo de porque os componentes precisam estar sincronizados com o clock. O Arduino não sabe quando ele precisa ler dos pinos, então fica lendo que nem doido e não faz sentido nenhum. Ligando ao clock, ele vai ler só uma vez a cada pulso. Entao em vez de fazer um loop infinito vamos ligar o código de leitura ao evento de um pulso do clock. Assim, se pausarmos o clock o programa também vai pausar, porque nenhum evento vai ativar o código de leitura.</p>
<p>Podemos ligar o clock em qualquer lugar, mas o Ben decidiu ligar no pino 2 do Arduino e com isso também precisamos configurar o pino 2 como entrada, pra ler o pulso que vier de lá.</p>
<p>A forma de ligar um evento no Arduino é criando uma interrupção pro clock. Quando o clock pulsa e enviar um sinal elétrico no pino 2, isso vai interromper o sistema e chamar essa função onClock que estamos definindo. Se você já programou front-end ou javascript já viu o mesmo conceito que é orientação a eventos. O conceito existe desde a invenção dos computadores, só pra saber.</p>
<p>Agora movemos o código do loop de leitura pra dentro do evento onClock.</p>
<p>Aproveitamos pra declarar também os pinos de 39 a 53 onde vamos ligar os pinos de dados D0 a D7 do CPU, pra poder monitorar o que acontece lá também.</p>
<p>Agora copiamos o mesmo código que lê dos pinos de endereço da CPU pra ler os pinos de dados da CPU, é o mesmo loop, só muda os números dos pinos.</p>
<p>Também copiamos o mesmo código de inicialização pra configurar os pinos 39 a 53 como INPUT pro Arduino receber bits.</p>
<p>E copiamos o mesmo código no evento de onClock pra ler os pinos de dados e imprimir na tela. Só que ficar mostrando os 24 bits de zeros e uns dos pinos não ajuda muito pra saber o que tá acontecendo. Mesmo gente experiente não consegue ler um monte de blocos de bits e entender exatamente o que tá rolando.</p>
<p>Então vamos converter de bits pra hexadecimal e imprimir do lado. Pra fazer isso inicializamos uma variável com número 0, dai pra cada pino que lemos nesse <code>for</code>, damos um shift pra esquerda e adicionamos o novo bit no final, faz shift pra esquerda, adiciona no final, faz shift e adiciona, e é assim que se monta um número lendo do binário.</p>
<p>Fazemos a mesma coisa com os pinos de dados.</p>
<p>Agora vamos usar uma função que é comum em quase todas as linguagens que serve pra formatar números em strings. Queremos formatar o número em uma string hexadecimal de 4 bytes pra representar o endereço de 16-bits, e como 2 bytes o valor de dados.</p>
<p>Os pinos de dados podem tanto receber dados quanto enviar dados e pra isso serve esse outro pino do lado, pra dizer qual o modo, se é read ou write. Então ligamos ele no pino 3 do arduino pra sabermos se o CPU está querendo ler ou escrever.</p>
<p>Como fizemos com os outros pinos, declaramos no codigo como 3 que é onde vamos ligar o pino RW do CPU. Dizemos ao Arduino pra ler desse pino, ou seja, input. Quando ler do pino, se devolver true escrevemos &ldquo;r&rdquo; pra leitura, se devolver false, escrevemos &lsquo;W&rsquo; pra indicar write.</p>
<p>Pronto, agora iniciamos o clock. No monitor podemos ver que o CPU tá cuspindo alguma coisa nos pinos de endereço e de dados mas tá difícil de entender o que tá acontecendo. Isso porque não tem nada enviando dados ao CPU, daí quando ele tenta ler alguma coisa dos pinos de dados vem lixo, e tenta rodar esse lixo e fica soltando lixo, por isso parece aleatório.</p>
<p>Agora que temos o CPU ligado e monitorado, vamos enviar o primeiro dado de verdade pra ele. Lembrem-se, enviar ou receber dados é literalmente aumentar ou diminuir voltagem nos pinos. Envie 5 volts no pino D0 e o CPU vai ler o bit 1 do pino D0, corte a voltagem do pino D1 e o CPU vai interpretar como 0 no pino D1. Então se quisermos mandar um valor constante nos pinos de dados, podemos ligar resistores. Se quisermos o bit 0 ligamos o resistor no terra, se quisermos o bit 1 ligamos o resistor nos 5 volts da placa. E com isso configuramos o binário 1 1 1 0 1 0 1 0.</p>
<p>Voltando ao monitor, resetamos e veja que na última coluna vem <code>ea</code> que é o hexadecimal do binário que acabamos de configurar. Como não mandamos o CPU gravar, o pino de Read/Write tá só Read, e note como nos pinos de endereço, a cada clock ele vai incrementando o endereço. Vamos ver com mais calma passo a passo.</p>
<p>Esse clock simples do Ben tem a opção de pausar e manualmente soltar um pulso de clock de cada vez clicando num botão. Segundo o manual do processador os primeiros 7 clocks é uma rotina de inicialização do CPU então podemos ignorar.</p>
<p>Logo em seguida o contador de programa carrega o vetor de reset dos endereços FFFC e FFFD. Como o único dado que conectado é o 0xEA dos resistores, ele vai ler o endereço 0xEA e 0xEA e montar o endereço 0xEAEA que é pra onde o CPU vai procurar a primeira instrução do programa pra executar.</p>
<p>Ele vai colocar 0xEAEA nos pinos de endereço e a única resposta que  vai ter continua sendo 0xEA, de novo porque é o único dado fixo nos pinos de dados. O Ben escolheu 0xEA de propósito porque é um opcode válido do 6502, se olharmos qualquer documentação dessa CPU vamos descobrir que é o opcode NOP ou No operation, que é literalmente um comando que não faz nada por 2 ciclos.</p>
<p>Quando o primeiro NOP terminar, o contador de programa aponta pro próximo endereço que é 0xEAEB e de novo, nos pinos de dados só vai ler só 0xEA. Daí o CPU executa NOP e o contador de programa agora vai pra OxEAEC e assim por diante infinitamente.</p>
<p>Até aqui você acabou de aprender alguns dos principais elementos que tem em qualquer CPU. Primeiro que ele tem pinos pra enviar endereços, segundo que ele tem pinos pra enviar e receber dados, que ele tem um contador de programa que aponta pra endereço da próxima instrução do programa. Terceiro que instruções e dados são tudo binário, e que não existe diferença entre um byte de dados e um byte de instrução.</p>
<p>Mas só deixar um valor fixo com resistores não é muito interessante. Pra ficar melhor, precisamos de alguma coisa que consiga enviar uma sequência maior de bytes, formando um programa.</p>
<p><a href="https://www.youtube.com/watch?v=yl8vPW5hydQ"target="_blank" rel="noopener">https://www.youtube.com/watch?v=yl8vPW5hydQ</a> (part 2)</p>
<p>Pra isso o Ben escolheu uma EEPROM do mesmo fabricante desse clone de 6502 que estamos usando, pra facilitar. Vale explicar o que é isso. RAM acho que todo mundo tem noção do que é, o que chamamos de memória. Podemos escrever e ler dados da memória e quando o computador desliga o que está na RAM apaga. Daí temos ROM que é sigla de Read only memory, ou seja é uma memória que só dá pra ler o que tem dentro e não é possível sobrescrever nem apagar.</p>
<p>Só pra ter um modelo simplificado na cabeça, pense na ROM como se fosse uma grade. Pense em cada linha dessa grade como um endereço, de 0 até X. E digamos que cada linha dessa grade tenha 8 cruzamentos. Quando um cruzamento está aberto, podemos dizer que é o bit 1, quando um cruzamento estiver queimado, pense queimado mesmo, ou seja que não tem como recuperar e mudar o bit. Podemos dizer que é o bit 0. O conteúdo de uma ROM é fixo, não tem como regravar. Você só consegue enviar endereços e ler o que estiver nesses endereços.</p>
<p>Agora, EEPROM é um circuito mais complicado. Quando ligamos num computador ele simula uma ROM, ou seja passe um endereço e ele devolve um dado. Porém, usando um gravador especial, ele permite que a gente escreva nela. Na prática uma evolução desse EEPROM são chips de memória flash usados em pendrives ou SSDs por exemplo. EEPROM é sigla pra Electronically-erasable programmable ROM. Nos anos 90 eu usava EEPROMs pra gravar um boot em computadores da rede da faculdade pra bootarem dessa ROM e se conectarem na rede pra baixar o sistema operacional de verdade. Dá pra usar EEPROMs pra muita coisa. Seria como bootar de um pendrive hoje mas a vantagem na época é que era difícil de pular esse boot obrigatório.</p>
<p>Diferente de RAM, mesmo quando desligarmos a força, o conteúdo não vai se apagar, igual um pendrive. Tanto nos videos do Ben quanto aqui vamos ficar toda hora chamando isso de ROM porque é mais curto, mas lembre-se que é uma EEPROM. E no caso é um modelo até grandinho, de 32 kilobytes.</p>
<p>Se olhar o diagrama vemos que a pinagem é muito simples, ele tem pinos de IO0 a IO7 pra devolver dados e pinos de A0 a A14 pra receber endereços. E porque vai só até A14 e não até A15. Porque ele só tem 32 kbytes que é metade de 64 kbytes. Com 16-bits conseguimos contar de 0 até 64kbytes, com 15-bits, que é metade conseguimos mapear os 32 kbytes que tem nessa ROM, por isso não precisa de mais 1 bit. Tão começando a se acostumar com essa história de bits? Basta associar bit com um pino num chip desses.</p>
<p>Repetindo, com 16-bits podemos mapear de 0x0000 até 0xFFFF.
Prestem atenção no comprimento dessas palavras. Cada dígito representa 4-bits. Cada 2 dígitos é um byte. 16-bits é a mesma coisa que 2 bytes, por isso 4 dígitos. Metade de 16-bits é só 1 bit a menos, o que nos dá os endereços de 0x0000 até 0x7FFF.
Se o CPU tentar mandar um endereço de 0x8000 pra cima, o primeiro bit é ignorado e o restante é igual à primeira metade, então ele devolve a mesma coisa que devolveria com o endereço de primeiro bit 0. Por exemplo, se ele pedir o endereço 0x8004 é a mesma coisa que pedir o endereço 0x0004.</p>
<p>Note outra vantagem de usarmos hexadecimais. Como eu sei exatamente qual é o numero do meio entre 0 e 65353?
Eu disse que pra dividir um numero por dois é só fazer shift pra direita, daí 1111, por exemplo, vira 0111. E esse é o numero do meio. E em hexa? qual a metade de F? É 7 porque F é 15, então metade de FFFF é 7FFF. Que é até onde vai o endereço nessa ROM. Tão vendo porque usamos hexa? Com decimal não é intuitivo de trabalhar. Aliás, em decimal, 7FFF em hexa seria 32767.</p>
<p>Como o CPU consegue entender qualquer endereço entre 0x0000 e 0xFFFF não queremos que a ROM responda em todos. Aqui é uma decisão de engenharia, mas pra manter a lógica simples, podemos escolher fazer a ROM responder à primeira metade dos endereços ou à segunda metade. E o Ben resolveu fazer responder à segunda metade, ou seja, quando o primeiro bit do endereço for &ldquo;1&rdquo;. Isso porque já vimos que o CPU procura logo de cara nos endereços FFFC e FFFD que ficam na segunda metade dos endereços, logo é mais conveniente mapear direto pra lá.</p>
<p>Pra fazer isso funcionar queremos responder somente a endereços que comecem com primeiro bit sendo &ldquo;1&rdquo;, ou seja, quando o pino A15 por &ldquo;1&rdquo;. Aliás, se vocês não perceberam a ordem dos bits é o inverso da numeração dos pinos, acostumem-se com isso. Além disso, pra ROM ficar habilitado ele precisa receber baixa voltagem no pino de chip enable ou CE. É como um botão de liga e desliga da ROM, pra ele responder precisa estar habilitado também.</p>
<p>Isso é simples, colocamos um inversor entre o A15 e CE assim quando A15 estiver em alta voltagem, ou seja, representando o bit 1 - porque queremos todos os endereços que começam bom 1 -, o inversor vai reverter e enviar baixa voltagem, ou 0, pro CE que vai habilitar a ROM. O resto dos pinos podemos ligar direto da CPU pra ROM um pra um, A14 da CPU pra A14 da ROM, D0 da CPU pro IO0 da ROM e assim por diante.</p>
<p>Coisas como esse inversor que mencionei pode ser montado com uma porta lógica chamada NAND. Eu mencionei portas lógicas no começo do episódio de Supremacia Quântica, mas o canal do Ben tem videos detalhados explicando como transistores funcionam e como podemos montar portas lógicas como NAND num breadboard também. Pro protótipo de hoje ele usa um chip pré-pronto com portas NAND embutidas. Mas entenda que todos os chips que você estão vendo são montados com transistores. Imagine transistores como peças de lego e chips como sendo um conjunto de transistores conectados.</p>
<p>Como falei antes, é com a relação de como montamos circuitos com transistores que nasce a base da tal &ldquo;lógica&rdquo;. Como conectamos portas pra chegar no resultado que precisamos. Neste exemplo simples, queremos uma resposta sempre que o pino A15 estiver em alta, com bit 1. Mas a ROM precisa do pino CS em baixa, ou bit 0 pra ativar. Portanto, colocamos alguma coisa que inverte o sinal entre elas, um inversor ou porta NAND, e assim vamos completando o quebra cabeça.</p>
<p>Com a ROM conectada e pronta precisamos carregar algum programa nela. Pra isso o Ben vai usar Python pra criar um arquivo binário. Poderíamos usar o próprio <code>xxd</code> pra escrever na mão. Mas qualquer linguagem de programação também consegue fazer a mesma coisa, use o que você achar mais fácil, mas vamos seguir no código dele que é simples de entender.</p>
<p>Relembrando que antes a CPU recebia somente o valor fixo dos registradores, o  hexadecimal 0xEA que é a instrução ou opcode NOP. Vamos gerar um binário que devolve exatamente a mesma coisa.</p>
<p>Primeiro declaramos uma variavel que vai ser um array de bytes. O conteúdo vai ser 0xEA 32 mil 768 vezes que é o total de bytes que dá exatamente 32 kilobytes.</p>
<p>Agora vamos abrir um arquivo pra escrever em modo binário. Com o arquivo aberto, escrevemos o array de bytes direto e isso vai gerar o arquivo &ldquo;rom.bin&rdquo;</p>
<p>Se fizermos um hexdump podemos ver que só tem 0xEA. Quando tem um asterisco é a ferramenta <code>hexdump</code> querendo dizer que o mesmo conteúdo se repete até o fim, que é o endereço 7FFF, o último em 32 kilobytes.</p>
<p>Lembrando que a CPU vai pedir endereços a partir de 0x8000 mas como a ROM não recebe o primeiro bit, só recebe os 15-bits restantes do endereço, então o endereço 0x8000 vira automaticamente endereço 0, como já expliquei.</p>
<p>Agora podemos pegar o arquivo binário e usar o programa <code>minipro</code> que envia os bits pro gravador de EEPROM. Isso é só um detalhe relevante se você for usar um gravador de EEPROM como na demonstração do Ben, mas é assim que se grava, se você tinha curiosidade.</p>
<p>Finalmente colocamos a EEPROM de volta na placa. E ligamos o Arduino de novo pra monitorar.</p>
<p>Podemos usar o mesmo script do Arduino de antes e ligar o Monitor e como esperado tá recebendo só 0xEA.</p>
<p>Pra ver passo a passo damos um reset e pausamos o clock. Agora podemos ir passo a passo. Os primeiros 7 clocks é a inicialização do CPU, depois disso ele lê dos endereços fffc e fffd e lê 0xea como antes. E com isso ele pula pro endereço 0xeaea e continua lendo só 0xea. Até aqui replicamos exatamente o que os registradores estáticos faziam. Vamos fazer algo um pouco mais útil.</p>
<p>Voltamos pro programa em python e depois de ter o array de bytes com 0xea, acessamos a posição 0x7ffc e colocamos o valor 0 e na posição 0x7ffd colocamos o valor 0x80. Duas coisas, lembram na explicação de binário que eu mostrei exemplos de Javascript, que expliquei que podemos acessar posições num array tanto com números decimais quanto com números direto em binário? Escrever 0x7ffc é a mesma coisa que escrever 32764.</p>
<p>E repetindo, por que 0x7fffc e não 0xfffc que é o que o CPU vai procurar? Porque a CPU vai mandar o endereço 0xFFFC pra ROM, mas o primeiro bit é ignorado? Lembra como o endereço 0x8000 vira 0x0000? Então é só fazer 0xFFFC - 0x8000 e voilá temos 0x7FFC. Não entendeu? 0xF em hexa é 15, subtrai 8 e temos 7.</p>
<p>Agora uma coisa que eu deixei pra explicar só agora. O CPU vai pedir o valor do endereço 0xFFFC e vai encontrar 0x00 e daí vai pedir 0xFFFD e vai encontrar 0x80 e esses dois valores vão formar o endereço 0x8000. Essa forma de ordenar os bytes &ldquo;ao contrário&rdquo; se chama Little endianess. Outros processadores como um Intel ou AMD ou mesmo os ARM de celulares também são little endian. O processador IBM PowerPC dos Macs anteriores a 2006 eram Big Endian, então os bytes iam aparecer na ordem natural que nós humanos lemos. Mesmo os processadores ARM de hoje são bi-endian, quer dizer que ele suporta ambos mas a maioria é usado como little endian.</p>
<p>Parece meio estranho, porque eu ia querer ler números de mais de um byte em ordem reversa? Na realidade parece estranho porque a gente lê da esquerda pra direita, mas quando implementamos em hardware é mais simples ler da direita pra esquerda, requer menos ciclos pra fazer contas. Sem entrar em detalhes do hardware vamos dar um exemplo onde a ordem natural pode não facilitar nossa vida. Imagine datas no formato brasileiro com dia, mês e ano. Mande um computador ordenar. Podemos usar o comando <code>sort</code> que tem em todo linux. Porém coloque as mesmas datas agora em formato japonês, que é ano, mês e dia e tente ordenar da mesma forma. E veja só, só de mudarmos a ordem dos elementos das mesmas datas, a ordenação funcionou muito mais fácil.</p>
<p>Aliás isso é um insight que todo programador precisa ter. Boa parte do que é programação é retrabalhar os dados pra ficar mais fácil de computar o que queremos. Little endian no nível do processador é algo parecido na hora de fazer contas. Pra agora só assuma que quando você vê dois bytes que representam endereço de 16-bits, eles vão estar invertidos e 0x0080 vai ser 0x8000.</p>
<p>01012000
01031998
02071999
03042003
05052005</p>
<p>20030403
19980301
19990702
20050505
20000101</p>
<p>Uma vez eu justamente precisei fazer um pequeno protocolo de rede binário pra trafegar dados entre computadores Intel e PowerPC. Acho que eu estava usando um Macbook G4 antigo. E eu recebia lixo no lado do Intel. Até que me dei conta &ldquo;puts, PowerPC é Big Endian&rdquo;, daí eu inverti os bytes no meu protocolo antes de mandar e do lado do Intel passou a receber direitinho. Você não vê isso porque se o único protocolo que já usou foi HTTP na camada de baixo no nível do TCP ele se vira pra reverter os bytes se trafegar entre processadores com endianess diferentes. Se você já usou algo como Google Protobufs, a biblioteca se encarrega de converter se precisar.</p>
<p>Isso tudo explicado, vamos fazer um programinha agora. Lembrando, um CPU tem registradores e pode guardar dados de 8-bits, ou 1 byte neles. Pense em registradores como se fossem variáveis globais numa linguagem de programação qualquer. Elas vão ser uma ordem de grandeza mais rápidas de acessar do que se tiver que sair do CPU e pedir memória pra um chip de RAM por exemplo. Registradores que podemos usar no 6502 incluem o A, X, Y e outros. Cada registrador vai ter um comando de LOAD pra jogar um valor nele e um comando de STORE pra tirar o valor do registrador e jogar em algum outro lugar na memória, se tiver RAM por exemplo.</p>
<p>Vamos só fazer um LOAD em A que é o opcode LDA. Assim como em linguagens modernas existe o conceito de polimorfismo, ou seja, a mesma função responder pra argumentos diferentes, no nível de máquina o mesmo comando LDA na verdade é representado por diferentes hexadecimais pra cada variação de argumentos. Por exemplo existe a instrução 0xAD, mas a versão que queremos é a que aceita só um valor simples e não um endereço de memória como argumento, então o hexadecimal da instrução LDA queremos é o 0xA9.</p>
<p>De volta ao python se acessarmos o primeiro valor do array, posição 0x00, colocamos o valor 0xA9 que é o comando LDA e na sequência colocamos o argumento dessa instrução que pode ser qualquer valor de 1 byte, aqui colocamos 0x42 arbitrariamente. A instrução completa são esses dois bytes. Dependendo da instrução pode ter mais argumentos, usando mais bytes na sequência. O CPU sabe quantos bytes cada instrução precisa. Tudo tem tamanho fixo em bytes na CPU, e assim ele sabe qual byte é uma instrução e quais bytes são dados.</p>
<p>Como agora temos um valor no registrador A podemos fazer alguma coisa com isso, tipo escrever nos pinos de dados. Pra isso usamos o STORE from A ou Grave a partir de A, que é o comando STA e ele recebe um argumento de 2 bytes que é o endereço pra onde queremos enviar o valor de A.</p>
<p>Então acessamos a posição 2 do array e colocamos o binário 0x8D que é a instrução STA. Agora nas posições seguintes queremos enviar esse dado pro endereço 0x6000, como já expliquei queremos o inverso disso então na posição 3 do array colocamos 0x00 e na posição 4 colocamos 0x6000.</p>
<p>Essa sequência de 3 bytes vai fazer o seguinte: escrever o valor que está no registrador A nos pinos de dados, mudar o bit no pino de read write pra escrever e mudar os pinos de endereço pra 0x6000.</p>
<p>Rodamos o script python pra gerar o novo binário, gravamos na EEPROM com o minipro de novo, recolocamos a ROM na placa e ligamos a energia e o monitor do Arduino.</p>
<p>Fazemos reset com clock pausado, e vamos manualmente clock a clock. Mesma coisa de antes, ignoramos os 7 primeiros clocks que é a inicialização. A CPU pede os valores de 0xfffc e 0xfffd que puxa da ROM o endereço 0x8000, esse é o reset vector e a CPU pede o que tem nesse endereço, que sabemos que é o endereço 0 da ROM e tem a instrução LDA que é 0x9a.</p>
<p>O contador vai incrementando a partir de 0x8000 e pede o que tem em 0x8001 que é o valor 0x42. A instrução está completa e a CPU grava no registrador. Agora o contador pula pra 0x8002, e de lá tem a próxima instrução STA que é 0x8d. O CPU vai incrementar o contador e ler os proximos dois bytes, que vai ser primeiro 0x00 e depois 0x60, esse é o endereço 0x6000.</p>
<p>Tudo funcionando conforme esperamos mas ao configurar os pinos pra gravar, não tem ninguém no barramento que responde a essas ordens, e nada acontece. No próximo ciclo esse dado vai sumir do barramento. Precisamos de alguma coisa pra segurar esse dado enquanto a CPU fica livre pra fazer outras coisas. Esse seria um componente chamado &ldquo;Latch&rdquo; que acho que podemos traduzir pra &ldquo;Tranca&rdquo;. Pra esse protótipo o Ben escolheu um chip que foi feito pelo mesmo fabricante desse clone de 6502 e é um adaptador que serve pra muitas coisas incluindo trancar alguns valores. Pense como se fosse um mini-memória.</p>
<p>Por acaso, os pinos desse adaptador são muito parecidos com o da ROM que já estamos usando, e tem espaço pra gravar até 2 dados de 8 bits no que ele chama de &ldquo;Portas&rdquo;. Tem uma Porta A de 8-bits e uma Porta-B de 8-bits. Por isso você tem pinos de PA0 a PA7 e pinos PB0 a PB7. A essa altura vocês já devem ter entendido a idéia de pinos serem cada um 1 bit né?</p>
<p>Diferente da ROM não precisamos de 16 pinos pra endereço porque esse chip só consegue armazenar no 2 valores. Sabemos que queremos ativar esse chip quando passarmos endereços que comecem com 0x60 que em binário é 0110 0000. Resumindo o que o Ben explica no video dele, ele vai ligar os endereços A15 a A13 com os pinos de chip select que são CS1 e CS2 e colocar um inversor e outra porta NAND no meio deles pra configurar o chip corretamente. Os detalhes não importam pra onde eu quero chegar, mas recomendo que assistam o episódio dele pra entender os detalhes.</p>
<p>O importante é entender que quando a CPU colocar endereços como 0x6000 ou 0x6001 ou 0x6002 e assim por diante, esse chip vai entender os primeiros 3 bits, ligar e receber o dado de 8 bits dos pinos de dados e gravar na Porta A ou Porta B. Quem indica a operação é o segundo byte do endereço. Entenda, o CPU chama os pinos de endereço, mas são bits, podemos ligar esses pinos com qualquer outra coisa pra montar nossa lógica.</p>
<p>No chip de Tranca, segundo a documentação ele espera receber 4 bits de configuração em 4 pinos registradores diferentes. Podemos ligar esses pinos aos pinos A0 até a4 que são os últimos bits do endereço de 16-bits. Se mandar 0 0 1 0 que é o número 2 ele direciona pra Porta B e se depois mandar 0 0 0 0 ele manda gravar efetivamente na Porta B, portanto a CPU precisa mandar o endereço 0x6002 pra ao mesmo tempo habilitar e configurar a direção e depois mandar o endereço 0x6000 pra de fato gravar o que estiver nos pinos de dados na Porta B. Parece complicado mas é bem simples, vamos ver.</p>
<p>Pra ver isso em funcionamento podemos ligar alguns LEDS nos pinos da Porta B. Assim quando o valor for trancado, ele vai ligar os LEDs correspondentes.</p>
<p>De volta ao python mas antes de continuar, é uma boa hora pra refatorar esse código pra ficar mais legível. É assim que se programa, primeiro fazemos a coisa mais simples que funciona primeiro. Agora vemos padrões no código que dificulta continuar, então pensamos em formas de tornar o código mais fácil de manusear.</p>
<p>Um das formas de fazer isso é definir um novo array de bytes em cima do array cheio de 0xEA&rsquo;s, esse array vai conter nosso programa exatamente igual antes. É só colocar os bytes que estávamos posicionamento manualmente embaixo, em sequência em cima e apagar o que tava embaixo.</p>
<p>Agora o array de ROM vai ser esse novo array de código mais o array preenchido com 0xea, só que em vez de ser vezes 32768, vai ser 32768
menos o comprimento do novo array de opcodes. Assim o array do binário vai ter exatamente o mesmo comprimento de antes e isso é importante porque tem que ser tudo preciso.</p>
<p>Escrever tudo numa linha só ou dividir em multiplas linhas pro python não faz nenhuma diferença, só faz diferença pra gente conseguir ler mais fácil. E de proposito colocamos dois bytes numa linha porque o primeiro é o hexa da instrucao LDA seguido do seu argumento. E na segunda linha colocamos 3 bytes onde o primeiro é a instrucao STA seguido de 2 bytes que formam o endereço pra onde gravar o conteudo de A.</p>
<p>Esse código é exatamente igual o que tínhamos antes, mas agora temos a Tranca que exige primeiro uma instrucao pra dizer a direçao pra saber qual porta usar. Então precisamos mandar o endereço 0x6002. Mas também precisamos mandar algum dado pelos pinos de dados. Se mandar tudo 0 ele habilita a porta B pra ser input e se mandar tudo 1 ele habilita a porta B pra ser output e gravar o que receber.</p>
<p>Enviar tudo 1 em 8 bits é o hexa 0xFF. Então mudamos o nosso array de código pra em vez de carregar aquele 0x42 vamos carregar com o número 0xFF. E em vez de mandar esse valor pro endereço 0x6000, vamos mandar pro 0x6002, e com isso setamos a direção do chip pra porta B e pra ela ser output.</p>
<p>Na sequência podemos fazer o que queríamos antes, travar um valor, digamos o hexa 0x55. Agora é só mandar a instrução STA pro 0x6000. Por acaso 0x55 em hexa é 0101 0101 e isso vai ligar os leds alternando o primeiro apagado, o segundo aceso e assim por diante.</p>
<p>Pra ficar mais legal podemos agora repetir os comandos, e mandar armazenar 0xaa em hexa que é 1010 1010 em binario, alternando os bits com o valor anterior, dai os leds apagados vao acender e os acesos vao apagar. Só uma brincadeira pra mostrar como podemos controlar alguns LEDs.</p>
<p>Pra finalizar seria legal colocar essas duas sequências num loop pros leds ficarem piscando sem parar, repetindo essas sequências. E a forma de se fazer loop, segundo a documentação do 6502 é com um jump que é a instrução JMP. Ele manipula o contador de programa pra apontar pra outro endereço qualquer, fazendo o CPU continuar incrementado de lá.</p>
<p>Então podemos fazer JMP pra 0x8000 que é o endereço 0 na ROM e começar do zero, mas não tem necessidade de reiniciar tudo. Um jump pro endereço 0x8005 é suficiente, ou seja, pulando os primeiros 5 bytes. Agora podemos rodar o script, gerar o novo binário, gravar na EEPROM, ligar e testar. E pronto, LEDs piscando como esperado.</p>
<p><a href="https://www.youtube.com/watch?v=oO8_2JJV0B4"target="_blank" rel="noopener">https://www.youtube.com/watch?v=oO8_2JJV0B4</a> (parte 3)</p>
<p>Se você chegou até este ponto e tá entendendo mais ou menos, no mínimo já deve tá ficando meio ansioso de ver esse monte de bytes num array. Neste ponto o Ben resolveu explicar uma forma menos feia de gerar esse binário. Obviamente ninguém escreve código em binário direto assim.</p>
<p>Mas considere por um instante que nos anos 50 pra trás era exatamente assim que precisava escrever. E a forma de fazer isso era com fios. Sabe os pinos de dados e endereço que ligamos registradores e agora ligamos a ROM? Antigamente era um patch panel. Lembra aqueles seriados ou filmes antigos dos anos 50 que mostra uma telefonista conectando uma ligação colocando ou tirando fios de um painel? É algo parecido. Configura os bits e manda pro computador, um conjunto de bits de cada vez.</p>
<p>Se você já leu alguma mini biografia do Bill Gates e do Paul Allen, vai lembrar que o primeiro computador comercial pra qual eles escreveram Basic foi o Altair 8800 que vinha com o processador Intel 8080 de 8-bits. Isso foi um ano antes de aparecer o processador Motorola 6800 que foi a base pro 6502 que estou mostrando aqui. Se você nunca viu um Altair veja esta foto, tá vendo esses switches no painel? A etiqueta em cima deles tá escrito D0 a D7 e embaixo A0 a A15. Agora você já sabe pra que serve esses switches.</p>
<p>Você tinha que escrever bit a bit subindo ou descendo esses switches, um a um os bits de dado e os bits de endereço e depois puxar o switch de &ldquo;DEPOSIT NEXT&rdquo; que tem nesse painel pra registrar na memória e fazer isso instrução a instrução, que é o equivalente do nosso array de bytes. Se você tá achando tedioso o que estamos fazendo, imagina escrever um programa inteiro só com esses switches, bit a bit, e sem errar nenhum bit. Essa é a verdadeira definição de &ldquo;escovar bits&rdquo;. Felizmente os computadores caseiros que vieram depois ganharam teclados e monitor e  isso facilitou ordens de grandeza a programar.</p>
<p>De qualquer forma, havia uma opção um nível acima de escrever binários direto, e é escrever em Assembly. E é assembly com &ldquo;y&rdquo; no final, que é escrever usando mnemônicos como usar a palavra LDA em vez de escrever o binário pra 0xa9.</p>
<p>Em vez de escrever direto 0xa9 0xff podemos escrever LDA # $ F F. O dólar indica que estamos escrevendo um número hexa e o hash significa que é um valor imediato e não um endereço. Quando compila sem o hash apesar do mnemonico ser LDA, temos um tipo de polimorfismo como já expliquei. Se passarmos um valor com hash vamos escolher o 0xa9, mas sem o hash, FF é um endereço e o opcode é diferente, o 0xa5.</p>
<p>Continuando em vez de 0x8d 0x00 0x60 podemos escrever STA $6000, ou seja, também não precisamos nos preocupar com a inversão de little endian como falei antes e escrever na ordem natural de leitura humana.</p>
<p>Nas linhas seguintes fazemos lda 55, sta 6000,
lda 11, sta 6000, jmp 8005</p>
<p>É exatamente a mesma coisa que fizemos no binário. Pra declarar que o início do programa é o endereço 0x8000 colocamos um indicador no começo do arquivo <code>.org 8000</code> que declara a origem. Lembre-se, na ROM o endereço inicial vai ser 0x00 mas do ponto de vista do CPU esse é o endereço 0x8000.</p>
<p>No final do código colocamos um novo <code>.org fffc</code> que é o primeiro endereço que o CPU sempre vai pedir pra buscar qual é o próximo endereço onde tem a primeira instruçao do programa, já repetimos isso várias vezes. E aí colocamos um word 0x8000 que indica o endereço inicial pro CPU buscar. Word é como chamamos um conjunto de 2 bytes.</p>
<p>Salvamos esses comandos num arquivo texto e agora podemos passar isso pra um Assembler, com &ldquo;er&rdquo; no final, que vai traduzir esse texto em binário. Em inglês Assembly que termina em &ldquo;y&rdquo; significa &ldquo;montagem&rdquo;, que é a instrução de montagem, ou o código-fonte. E Assembler que terminar com &ldquo;er&rdquo; é o montador, que vai pegar essa instrução de montagem e montar o binário propriamente dito.</p>
<p>Pra gerar o binário correto precisamos do Assembler específico pro 6502. Na maioria dos Linux basta instalar o pacote <code>vasm</code>. Pra quem tá em Windows ou Mac vou deixar o link do site nas descrições abaixo e o video do Ben explica em detalhes como usar.</p>
<p>Existem vários assemblers diferentes pra família 6502, do Apple II é diferente do Atari que é diferente do 6502 genérico. O assembler que precisamos se chama <code>vasm6502_oldstyle</code>. Se abrirmos o binário que ele gera com hexdump e compararmos com o nosso produzido com o script de python, vemos que é idêntico ao array de bytes que estávamos escrevendo na mão.</p>
<p>Pra completar os 32k vemos que faltou só 2 bytes no final, pra isso só adicionamos um novo word no fim com 2 bytes pra completar. Mas agora que estamos escrevendo em Assembly podemos usar funcionalidades do Assembler Vasm que melhoram um pouco nossa qualidade de vida de programação em baixo nível. A maioria dos Assembler implementam macros e outras coisas pra evitar que a gente tenha que ficar fazendo repetições ou decorando endereços de cabeça.</p>
<p>Por exemplo, lembra que temos um JUMP pra um endereço fixo 0x8005? E se colocarmos mais instruções antes disso? Toda hora ia precisar ficar atualizando esse endereço na mão. Em vez disso podemos adicionar um label e fazer o jump pular pra esse label e deixar o assembler calcular qual é o endereço certo.</p>
<p>A diferença no código é que precisamos sempre identar com espaços ou tab, e labels começam na primeira coluna sem identação. Só de fazer isso já nos livramos de um número hard-coded. Números mágicos como aquele sempre são ruins e em Assembly isso não é diferente.</p>
<p>Ainda dá pra melhorar o codigo. Podemos deliminar a região de reset com um label e só pra brincar, vamos mudar o valor 0x55 pra ser 0x50. No loop vamos tirar o que tinha antes e colocar uma instrução nova que é o ROR ou rotate right. Lembra o conceito de shift pra direita? Pense que 0x0050 é <code>0 0 0 0  0 1 0 1</code> daí quando der shift right vai virar <code>1 0 0 0  0 0 1 0</code> e a cada loop ele vai dar shift e rotacionar pra direita. Significa que vai ter só dois leds acesos a cada clock e eles vão se &ldquo;movimentando&rdquo; pra direita.</p>
<p>Podemos compilar com o Vawm pra gerar o binário, gravar na EEPROM e fazer o de sempre, colocar a ROM na placa, ligar força, resetar e ver se funciona. No caso ele tá rotacionando pra esquerda porque a placa tá de ponta cabeça no video, mas você entendeu.</p>
<p><a href="https://www.youtube.com/watch?v=FY3zTUaykVo"target="_blank" rel="noopener">https://www.youtube.com/watch?v=FY3zTUaykVo</a> (parte 4)</p>
<p>Com os LEDs fazendo o que esperávamos, sabemos que a trava está funcionando e gravando o dado na porta certa. Então podemos ir um passo além. Essa trava pode funcionar como um buffer pra uma tela e o Ben conseguiu uma tela de LCD simples. Novamente, vamos pular a configuração do hardware mas a parte importante é que ele recebe 8 pinos de dados como a ROM e o latch, e assim como o latch ele tem 3 pinos de controle onde podemos ativar configurações.</p>
<p>Em resumo o LCD pode ou receber instruções ou receber dados, que seriam as letras que queremos que apareça na tela. Instruções são coisas como limpar a tela, mudar a posição, configurar um cursor e assim por diante. O manual da tela tem os detalhes de cada configuração mas vou mostrar o valor que vamos mandar no Assembly. Pra receber dados vamos conectar os pinos de dados do LCD com os pinos da Porta B do latch, e pra configurar os 3 pinos de controle vamos ligar em 3 pinos da Porta A do latch.</p>
<p>Vamos refatorar o código de novo criando alguns simbolos ou constantes pro codigo ficar um pouco mais legivel. Criamos PORTB = 6000 e PORTA = 6001.
DDRB é a direção se vamos escrever na porta B o endereço é 6002
mas se quisermos escrever na PORTA temos que setar DDRA que é 6003</p>
<p>0xff significa tudo 1 em hexa, podemos deixar mais claro escrevendo o número em binario, bit a bit. Dizemos pro Assembler que isso é um número binário colocando % (porcento) no começo e ; (ponto e vírgula) é comentario, que não aparece no binário final.</p>
<p>Agora os pinos de controle do LCD, pra isso vamos setar só os primeiros 3 bits que são os que conectamos nos pinos de controle da tela. Agora é uma sequência específica desse modelo LCD, então não se preocupem tanto com os detalhes dessa parte.</p>
<p>Pra inicializar a tela começamos mandando 00111000. O primeiro bit seta modo 8-bits, o segundo seta 2 linhas na tela e o 3o  seta fonte de 5 por 8 pixels. Por fim um STA mandando pra PORTB.</p>
<p>Carregamos zero com LDA e mandamos Porta A com STA pra limpar os pinos de controle RS, RW e E. RS é o pino que seleciona se estamos mandando instruções ou dados pra tela, RW se estamos lendo ou escrevendo e E se tá habilitado pra receber comandos.</p>
<p>Agora queremos habilitar com LDA do valor E e depois desligamos esse enable. É uma sequencia bem burocrática mesmo, a maioria dos hardware tem alguma sequência de controle assim mesmo, mas é o que a documentação do LCD diz que precisa fazer. Mas já tá quase acabando.</p>
<p>Cada nova instrução que queremos mandar precisa copiar toda essa sequencia. Agora mandamos <code>0000 1110</code>. De novo 3 bits, mas agora é pra ligar o display, ligar o cursor e não fazer blink que é ficar piscando na tela.</p>
<p>Copiamos todo o bloco de controle e colamos embaixo. A última configuração é mandar <code>0000 0110</code> que é dizer pro LCD que pra cada nova letra que mandarmos é pra incrementar a posição e não fazer nenhum tipo de scroll.</p>
<p>E tem que copiar todo o bloco de controle de novo.</p>
<p>Finamente podemos enviar a letra que queremos escrever no LCD. Poderíamos dar LDA do número em binário ou hexa que representa a letra na tabela ASCII. Mas como estamos usando um Assembler, ele sabe converter sozinho então podemos escrever o string &ldquo;H&rdquo; entre aspas que ele se vira.</p>
<p>Daí mandamos pra Porta B com STA.</p>
<p>O bloco seguinte é um pouco diferente do que fizemos antes. Em vez de zerar os pinos de controle temos que ligar o pino que seleciona o registrador pra em vez de receber instrução, receber dado, entao configuramos com a constante RS que tínhamos declarado antes.</p>
<p>Na sequência, além de só habilitar com a constante &ldquo;E&rdquo;, tem que mandar junto o bit de RS. Pra isso usamos o operador bitwise de OR que é uma barra vertical. O que isso faz? Ele faz um OR bit a bit. De novo, isso é lógica. Na prática o resultado é mesclar os bits de RS e de E. Como RS é 0010 e E é 1000 o resultado vai ser 1010. Aqui valeria uma tangente mas como já está comprido só quero deixar um lembrete que essa é uma técnica muito usada especialmente pra quem mexe com protocolos de rede. Isso é como trabalhamos Bit Fields, procurem sobre isso no Google. É uma forma eficiente de enviar várias características de alguma coisa embutido num único byte em vez de ter um byte separado pra cada valor.</p>
<p>Por fim zeramos o bloco de loop pra quando chegar nele ficar num loop infinito e não fazer nada. Assim ele termina de escrever na tela e não faz mais nada.</p>
<p>Compila, grava a eeprom, e testamos. E aparece o &ldquo;H&rdquo; na tela como queríamos.</p>
<p>Pra escrever o resto das letras do hello world, por agora vamos fazer do jeito mais porco e sujo só pra ver o resultado rápido. Ou seja, copy e paste nervoso de toda a sequência da letra &ldquo;H&rdquo; pra cada uma das outras letras. Isso é obviamente péssimo de se fazer. Pra testar a primeira vez quando você ainda não sabe se vai dar certo tudo bem, mas obviamente vamos voltar e melhorar isso depois.</p>
<p>Vamos compilar e não só o código Assembly ficou longo, mas o binário reflete isso. Olha como o binario no hexdump ficou gigantesco com mais de 300 bytes só pra escrever um mísero hello world. Escrever código longo e sujo além de feio e ruim de manter, também gera instruções redundantes e aumenta o tamanho do binário.</p>
<p>Agora podemos transferir pra ROM, ligar na placa e, como esperávamos, temos um Hello World aparecendo. Se o objetivo for só escrever Hello World do jeito mais feio do mundo, já acabamos, mas podemos só acrescentar uma última coisa pra melhorar muito esse código.</p>
<p><a href="https://www.youtube.com/watch?v=xBjQVxVxOxc"target="_blank" rel="noopener">https://www.youtube.com/watch?v=xBjQVxVxOxc</a> (parte 5)</p>
<p>Vamos aumentar nosso arsenal de Assembly aprendendo o conceito de sub-rotinas, que é uma forma primitiva de funções. Aliás, pra quem já é programador e já ouviu falar do conceito de &ldquo;GO TO&rdquo; isso é o Jump pra um endereço fixo que mostramos com o mnemônico JMP. Mas nem em Assembly é obrigatório usar só jumps.</p>
<p>Pra refatorar esse codigo vamos criar uma subrotina chamada <code>lcd_instruction</code> que manda os comandos pra escrever os dados na tela depois do LDA. Toda aquela sequência que demos copy e paste várias vezes no começo do código.</p>
<p>Já sabemos que JMP pula pra um endereço fixo. O problema é que uma vez que você pula, não tem mais como voltar pra trás. Pra ter esse recurso podemos usar o opcode JSR que é Jump Sub Routine, que pula pra uma subrotina e grava o endereço atual do contador de programas. Daí, no final da subrotina usamos o comando RTS que é tipo <code>return</code> num Javascript, que vai pegar o ultimo endereço gravado pelo JSR e colocar de volta no contador de programas, e com isso o CPU continua na instrução logo depois do JSR.</p>
<p>Essa é a forma mais rudimentar de uma função em linguagem de máquina. Fazemos a mesma coisa criando uma subrotina <code>print_char</code>, chamamos JSR depois do load da letra e no final da subrotina colocamos RTS pra voltar pro byte seguinte da instrução de jump.</p>
<p>Depois de recompilar podemos comparar o tamanho dos binários, o anterior sujo tinha 333 bytes. Agora ficou bem menor em tamanho. Só que isso não sai de graça. A versão anterior era maior mas era pouca coisa mais rápido porque agora a CPU vai executar um monte de Jump e Return que não tinha antes. Cada uma dessas novas instruções usa clocks pra executar. Porém é aquele caso em que o ganho de performance não compensa a sujeira do código. Via de regra você sempre deve preferir código legível do que otimização prematura.</p>
<p>Isso faz parte do dia a dia de um programador: ter que escolher quando você escreve um pouco mais sujo pra ganhar performance e quando o ganho de performance é tão pequeno que não compensa. Não existe uma receita, só com experiência você vai saber. Mas via de regra você só toma essa decisão depois de conseguir testar o código e mensurar se o ganho vale a pena ou não. Nunca  ache que só porque um código rodou a primeira, já tá pronto e nunca mais vai mudar. Neste exemplo besta já refatoramos o código várias vezes, imagina num código grande de verdade.</p>
<p>Gravamos na ROM, testamos e &hellip;.  nada acontece, o programa parece que travou. Por que? Pra descobrir vamos ligar o monitor de Arduino de novo, resetar e colocar em clock manual pra ir linha a linha. Vamos acelerar direto pro suspeito mais óbvio que é o JSR.</p>
<p>Temos o LDA que vai configurar a primeira configuração pra tela. Em seguida temos o opcode 0x20 que é o JSR.</p>
<p>Deveríamos ver algo como um 0x20 e depois 0x5d e 0x80 representando o endereço 0x805d que é onde está a subrotina. Mas em vez em disso vemos essa sequencia estranha 5d 80 0e. Aí parece que volta ao normal continuando a instrucao com o 0x80 restante pra formar 0x805d. Ou seja, ele pula pra subrotina e podemos ver que ele segue normalmente.</p>
<p>Seguimos mais algumas instruções e chegamos no opcode 0x60 que é o RTS ou return, mas pra onde ele vai retornar? Deveria ser pra 0x800f que é o próximo endereço depois do último JSR em 0x800e.</p>
<p>Estamos vendo no monitor que o CPU tenta ler alguma coisa em 0x0123 e 0x0124. O lance é que JSR e RTS dependem de algum hardware que responde nesses endereços pra gravar o endereço de retorno, mas no momento nao temos nenhum hardware que responde.</p>
<p>Quando chama JSR ele tenta gravar nesses endereços, mas não tem ninguém esperando isso então o endereço de retorno vai pro limbo. Quando chega no RTS ele tenta recuperar o endereço, e não tem ninguém pra responder, então acaba tendo algum lixo nos pinos e o CPU assume que é um endereço e tenta pular pra lá. No caso veio um lixo 0x8d8d e o CPU vai pra lá mas não tem nada e o programa se perde.</p>
<p>Esses endereços 0x0123 0x0124 são específicos do processador 6502. Segundo a documentação, de 0x0100 a 0x01FF temos o Stack, uma pilha. E temos mais um registrador que ainda não mencionei, chamado SP. Todo CPU tem uma série de instruções implementadas em hardware como o LDA STA JSR e outros. Instruções como LDA grava no registrador chamado &ldquo;A&rdquo;. Até agora só vimos 2, o registrador A e o PC que é o contador de programa. E o ultimo registrador novo importante pra hoje é esse SP é o Stack Pointer, que guarda um endereço do topo de uma pilha. O SP é quem permite a existência do conceito de sub-rotinas e funções.</p>
<p>Stack ou Pilha é uma estrutura de dados. Lembra a tal fundação que eu sempre falo? Se você estudar estrutura de dados, vai descobrir a Pilha, uma das estruturas mais básicas e que você aprende logo no começo dessa matéria. Todo iniciante tem que saber o que é uma pilha e quando usar ela. Em resumo, é uma cadeia de elementos onde você vai colocando um elemento de cada vez no topo. E toda vez que pedir pra devolver um elemento, vai removendo do topo.</p>
<p>Essa pilha é um tipo particular chamado LIFO, Last In, First Out, ou seja, quem entra por último, sai primeiro. A vantagem de uma estrutura dessas é que basta guardar o endereço do topo. Depois que adiciona um elemento, decrementa o endereço do topo. Pra puxar da fila, devolve o dado do topo e incrementa o endereço no registrador SP. Quando falamos topo, não necessariamente começamos do zero e vamos subindo um a um. No caso do 6502 ele prefere iniciar no meio da pilha, em 0x0124 e vai subindo até 0 e quando chegar no zero ele rotaciona pro fim da pilha em 01FF e continua subindo.</p>
<p>Toda vez que você faz um JSR, ele vai acumular um endereço na pilha. Se você fizer mais que 255 jumps sem fazer um Return, ele vai estourar o tamanho da pilha e vai começar a sobrescrever o primeiro endereço gravado. É isso que se chama de transbordar da pilha ou, em inglês, Stack Overflow. Mesmo com uma pilha pequena dessas é muito difícil fazer 255 jumps sem retorno. Quando isso acontece normalmente é uma recursão mal feita. Recursão é uma sub-rotina que dá JSR pro começo dela mesma. É muito fácil um iniciante errar com recursão.</p>
<p>Outra coisa interessante de saber é que registradores são literalmente variáveis globais. Se você já trabalhou com C ou Javascript já deve ter ouvido falar de quão tenebroso uma variável global pode ser. Antes de fazer o JSR no nosso código, setamos o registrador A com um valor usando LDA, mas quando pulamos pra subrotina, dentro dela modificamos esse registrador A. Se depois que retornássemos o programa esperasse encontrar o mesmo valor original em A, teríamos problemas.</p>
<p>Como no nosso caso, depois que retornamos não faz diferença porque vamos escrever a próxima letra em cima do A mesmo, então tudo bem, mas se quiséssemos evitar algum efeito colateral num código maior, poderiamos primeiro empurrar o valor de A pro stack também, usando a instrução de push A que é PHA.</p>
<p>E antes de retornar com RTS podemos puxar o valor de A do topo do stack usando pull A ou PLA. Por garantia então devíamos usar isso no começo e fim de toda sub-rotina né? Mais ou menos, esse hardware antigo só tem 255 slots na pilha lembram? Se você souber que não vai estourar, pode fazer isso. Como eu falei, tudo é um trade-off. Se eu quiser garantias demais, os recursos podem acabam cedo demais e podemos acabar ganhando um bug de stack overflow sem querer.</p>
<p>Só por comodidade podemos mudar o registrador SP de 0x0124 pra 0x01FF pra começar do fim da pilha em vez do meio. Pra isso podemos procurar uma instrução como o LOAD A, mas nao tem LOAD pro registrador SP da pilha, e sim TRANSFER que é TXS, que transfere um valor do registrador X pro SP. E aqui aprendemos que existe um registrador chamado X além do A.</p>
<p>Assim como fazemos com o registrador A, primeiro usamos a instrução LOAD to X que é LDX com #$FF e daí transferimos X pra SP com TXS. Note que colocamos FF e não 0x01FF porque os endereços do stack sempre começam com 0 1 em binário entao na pratica estamos contando sempre de 00 a FF. Já expliquei que é um stack pequeno com 255 posições.</p>
<p>Tudo isso entendido, precisamos de algum lugar que tem espaço pra gravar 255 elementos de uma pilha e responde aos endereços de 0x0100 a 0x1FF. Nosso chip de trava só tem 2 portas então não serve. Chegou a hora de adicionarmos um chip de memória RAM. E fica outro insight, memória RAM pode ser opcional se o que você pretende fazer com um hardware são operações simples que sobrevivem só com registradores ou com uma trava simples.</p>
<p><a href="https://www.youtube.com/watch?v=i_wrxBdXTgM"target="_blank" rel="noopener">https://www.youtube.com/watch?v=i_wrxBdXTgM</a> (parte 6)</p>
<p>O Ben escolheu um chip de RAM que é muito parecido com a pinagem do EEPROM e da Trava que já estamos usando, então vamos pular a configuração do hardware que é a mesma coisa: liga força, liga terra, liga os pinos de dados e de endereço. Assim como todos os outros chips por acaso tem 3 pinos de controle que importam, um pra write enable que como o nome diz habilita escrita, um pra output enable e outro pra chip select. Write enable é fácil, porque o 6502 tem um pino read write que indica escrita, daí ligamos direto um no outro.</p>
<p>Assim como no caso do ROM e do LCD precisamos definir os endereços que a RAM vai responder. Sabemos que o stack precisa pelo menos dos endereços 0x0100 até 0x01FF. Mas vamos mapear o primeiro 1/4 dos endereços do topo que vai de 0x000 até 0x3FFF que é bem longe dos endereços 0x6000 que o LCD usa e do endereço 0x8000 que a ROM usa.</p>
<p>Isso é suficiente pra 16 kilobytes de RAM. Por acaso este chip moderno tem mais espaço que isso, então vamos desperdiçar um pouco neste protótipo. Pra usar tudo daria mais trabalho pra criar um mapeador de endereços. Mas lembre-se que um nintendinho tinha só 2 kilobytes de RAM e 2 kilobytes de memória de video. E no próximo episódio vou explicar porque não dava pra ter muito mais que isso mesmo.</p>
<p>Existe um pequeno circuito que precisa ser adaptado e vou pular essa explicação, mas em resumo a forma de conectar a RAM na CPU é ligar o A14 da cpu com o A14 da RAM cruzando com o output enable e o A15 da cpu de novo através de um inversor e um nand no chip select. O que precisamos saber é que isso vai habilitar a RAM quando o endereço começar com 0 0 que cobre os endereços que queremos. Pra entender porque a ligação em hardware é dessa maneira, veja o video do Ben.</p>
<p>Feito isso, agora temos RAM ligado e configurado respondem nos endereços que precisamos, então teoricamente já temos uma pilha disponível e com isso o programa deveria funcionar. Se ligarmos e dermos reset, de fato, agora o hello world aparece como deveria.</p>
<p>Tudo resolvido, vamos ligar o Arduino pra entender como o programa se comporta. Fazemos reset e deixamos o clock em manual de novo. Como sempre, vamos ter os 7 clocks pra inicializar a CPU e pulamos rápido até o ponto onde o programa faz o jump JSR que é o opcode 0x20.</p>
<p>Lembrando que configuramos o registrador SP pra começar de 0x01ff, que é o fim do espaço de endereços da pilha, e de fato vemos no monitor que tá escrevendo 0x80 no final do stack em 0x01ff. Daí o cpu atualiza o registrador SP pro próximo endereço ser 0x01fe e escrever 0x11 lá. 0x8011 é o endereço de retorno da subrotina.</p>
<p>Agora ele salta pra subrotina que tá em 0x8060, como já fazia antes, executa a sequência pra escrever a letra no LCE e chega na instrução RTS que é o opcode 0x60.</p>
<p>A instrução de retorno vai desempilhar o primeiro byte do topo da pilha, que está em 0x01fd, e lê 0x11. Daí decrementa o registrador SP e lê 0x80 e isso forma o endereço 0x8011 que é pra onde o programa tem que voltar. E com isso temos o suficiente de hardware pra fazer jumps o que garante que podemos programar praticamente qualquer coisa só com o que temos aqui.</p>
<p>Os dois registradores de controle de um CPU são o contador de programa ou PC que tem o endereço da próxima instrução ou byte que ele tem que ler. E o apontador de pilha ou SP que é o endereço do topo da pilha. Tendo esses dois controle você consegue executar uma instrução após outra e consegue dar jumps pra continuar executando de outro endereço e retornar pro endereço anterior. Ou seja, você tem o equivalente a um loop com WHILE numa linguagem de alto nível. Daí existem instruções de Branch e Compare que servem pra comparar valores e dar jumps dependendo das condições que você configurar, e isso é o equivalente a um IF numa linguagem moderna.</p>
<p>Com essas poucas instruções que vocês viram já dá pra ter o modelo da maioria das funcionalidades básicas de qualquer linguagem: loops, funções e ifs ou branches que não cheguei a mostrar mas acho que vocês já conseguem imaginar como seria. Na série do Ben tem um episódio extra depois que ele configurar o LCD onde ele troca o clock que está montado num breadboard por um clock de 1 Megahertz de verdade. Mas quando ele faz isso o programa pára de funcionar. Tudo funciona quando o clock é bem lento, mas quando o clock fica rápido demais, o CPU envia os comandos pro LCD rápido demais e o LCD não responde a tempo. Pra evitar isso precisa adicionar um comando no CPU pra perguntar pro LCD se já pode mandar a próxima letra.</p>
<p>De qualquer maneira eu queria passar pelos principais pontos dos videos do Ben pra que vocês pudessem ver como um CPU funciona, como ele se comunica com outros componentes como memória RAM ou um LCD simples. Como um CPU carrega um programa e executa. No fim do dia, não importa que linguagem você está usando, ele vai ser convertido em bytes de instruções parecidas como o que acabei de mostrar. Vocês viram que só de pular do binário pra escrever em Assembly já ganhamos algumas conveniências que o Assembler Vasm nos dá.</p>
<p>Quanto mais conveniências uma linguagem adiciona, mais abstrações ele faz e mais lento ele tende a ser. Como falei antes existe um trade off sempre, sempre existe um balanço entre ser conveniente e ser rápido, entre ser rápido e ser seguro e assim por diante. Em outros videos eu vou tentar explicar as diferenças das diferentes linguagens levando em consideração que vocês assistiram este vídeo.</p>
<p>No próximo video eu vou usar o que mostrei aqui de uma maneira mais prática. Eu mostrei a idéia do Game Genie no começo do video mas não fiz nada com ele hoje, é o que vou começar mostrando na parte 2. A parte 2 vai ser bem menos tedioso que esta parte 1, isso eu garanto, porque agora podemos usar tudo que eu mostrei aqui na prática em programas de verdade.</p>
<p>Muitos programadores associam &ldquo;linguagem de máquina&rdquo; com algo difícil e inatingível que não foi feito pra eles, mas isso não é verdade. Linguagem de máquina e o hardware em si são razoavelmente simples, peças de lego. Tudo funciona em cima das mesmas peças e você vai ser um programador melhor se pelo menos tiver uma imagem mental de como seu código interage com esses componentes.</p>
<p>Uma coisa parece difícil só porque você nunca viu. É que nem um truque de mágica. Mas depois que a gente revela o truque, as coisas ficam muito mais claras. Se você não entendeu o video todo, não se preocupe, eu literalmente resumi o equivalente a umas 5 horas de aula do canal do Ben Eater aqui. Vou deixar os links pro canal dele nas descrições. Se quiserem adicionar mais a esta conversa não deixem de mandar nos comentários abaixo, se curtiram o video mandem um joinha, assinem o canal e cliquem no sininho pra não perder o próximo episódio e, como sempre, compartilhem o video pra ajudar o canal. A gente se vê na próxima, até mais.</p>
<p>Se tem um tema que eu queria fazer desde que abri o canal é apresentar algumas das fundações da computação, tanto do ponto de vista de história e da evolução, mas principalmente dos fundamentos básicos que eram vários 60 anos atrás e ainda são válidos até hoje.</p>
<p>O video de hoje vai abrir com um tema de videogames, mas vamos descer bem fundo no cérebro do Nintendinho pra criar um vocabulário que eu vou usar na Parte 2, quando de fato vou mexer num emulador de Nintendo. E esse vocabulário vai servir não só pra videos futuros meus como pra tudo que você for fazer em computação. Esta é a base, sem esta base tudo que você tentar aprender de avançado depois vai ser mais difícil.</p>
<p>Infelizmente é muito difícil empacotar estes temas de uma forma que não seja maçante. Espero ter conseguido pelo menos interessar vocês no assunto.</p>
]]></content:encoded><category>ciencia-da-computacao</category><category>retrocomputacao</category><category>tutoriais</category><category>akitando</category></item><item><title>[Akitando] #70 - Entendendo GIT | (não é um tutorial!)</title><link>https://akitaonrails.github.io/2020/02/05/akitando-70-entendendo-git-nao-e-um-tutorial/</link><guid isPermaLink="true">https://akitaonrails.github.io/2020/02/05/akitando-70-entendendo-git-nao-e-um-tutorial/</guid><pubDate>Wed, 05 Feb 2020 10:30:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/6Czd1Yetaac"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIPTION&lt;span class="hx:absolute hx:-mt-20" id="description"&gt;&lt;/span&gt;
&lt;a href="#description" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Finalmente estou de volta depois de uns 2 meses de férias!&lt;/p&gt;
&lt;p&gt;Pegando o gancho que eu deixei no primeiro episódio da série Começando aos 40, em Conhecimentos Gerais eu falei que era obrigatório saber sobre Git. Pois bem, finalmente consegui fazer um episódio pra cobrir esse assunto.&lt;/p&gt;
&lt;p&gt;Porém, diferente de qualquer outro vídeo que daria um tutorial dos comandos básicos, eu não vou ensinar nenhum comando nem fluxo de trabalho. Em vez disso quero contar algumas histórias de como eu comecei a usar Git e o que me convenceu a insistir em usar mais de 10 anos atrás quando basicamente ninguém fora do projeto de desenvolvimento do kernel do Linux estava interessado.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/6Czd1Yetaac"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIPTION<span class="hx:absolute hx:-mt-20" id="description"></span>
    <a href="#description" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Finalmente estou de volta depois de uns 2 meses de férias!</p>
<p>Pegando o gancho que eu deixei no primeiro episódio da série Começando aos 40, em Conhecimentos Gerais eu falei que era obrigatório saber sobre Git. Pois bem, finalmente consegui fazer um episódio pra cobrir esse assunto.</p>
<p>Porém, diferente de qualquer outro vídeo que daria um tutorial dos comandos básicos, eu não vou ensinar nenhum comando nem fluxo de trabalho. Em vez disso quero contar algumas histórias de como eu comecei a usar Git e o que me convenceu a insistir em usar mais de 10 anos atrás quando basicamente ninguém fora do projeto de desenvolvimento do kernel do Linux estava interessado.</p>
<p>Vamos mergulhar fundo em entender o funcionamento e a estrutura básica do Git, e sabendo isso eu acho que você vai começar a se interessar muito mais em entender mais dessa ferramenta e talvez até olhar para o que você faz de maneira muito diferente!</p>
<p>Links:</p>
<ul>
<li>FLOSS Weekly 19 (<a href="https://twit.tv/shows/floss-weekly/episodes/19"target="_blank" rel="noopener">https://twit.tv/shows/floss-weekly/episodes/19</a>)</li>
<li>Git Internals (<a href="https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain"target="_blank" rel="noopener">https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain</a>)</li>
<li>Three-Way Merging: A Look Under the Hood (<a href="https://www.drdobbs.com/tools/three-way-merging-a-look-under-the-hood/240164902"target="_blank" rel="noopener">https://www.drdobbs.com/tools/three-way-merging-a-look-under-the-hood/240164902</a>)</li>
<li>Tech Talk: Linus Torvalds on Git - Transcript (<a href="https://gist.github.com/dukeofgaming/2150263"target="_blank" rel="noopener">https://gist.github.com/dukeofgaming/2150263</a>)</li>
<li>Longest Common Subsequence (2 Strings) (<a href="https://www.youtube.com/watch?v=ASoaQq66foQ"target="_blank" rel="noopener">https://www.youtube.com/watch?v=ASoaQq66foQ</a>)</li>
<li>TortoiseSVN mailing list 2004 (<a href="https://markmail.org/message/uwi6ld7jqwoyau6z"target="_blank" rel="noopener">https://markmail.org/message/uwi6ld7jqwoyau6z</a>)</li>
<li>Posts de Git em AkitaOnRails.com (<a href="https://www.akitaonrails.com/git"target="_blank" rel="noopener">https://www.akitaonrails.com/git</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal Fabio Akita</p>
<p>Finalmente tô de volta! Fiquei uns dois meses fora meio que de férias de fim de ano enquanto eu tava fazendo obras e me mudando pro meu novo apartamento. Ainda não tá 100% mas vocês já podem ver que o cenário mudou e este é o piloto de teste da nova configuração de equipamentos. Ainda vou refinar mais um pouco e em breve faço um vídeo sobre como tô gravando a partir de agora.</p>
<p>Como já estamos em fevereiro nem vou falar de retrospectiva nem nada disso. Vamos já pular direto pra um assunto técnico. Na real, eu tô postergando esse tema desde um ano atrás quando comecei a série Começando aos 40 e no primeiro vídeo de conhecimentos gerais eu disse que era obrigatório conhecer Git e Linux. Linux eu já fiz um episódio onde falei sobre como instalar e configurar um ambiente de desenvolvimento em Ubuntu. Então resta falar sobre Git.</p>
<p>Porém, já vou avisar que isso Não é um tutorial de Git. Uma coisa que eu não tô a fim de fazer é tutoriais que você pode facilmente encontrar em qualquer outro canal, ou curso online ou posts de blog. Aprender a meia dúzia de comandos básicos de Git é super simples, nenhum inconveniente. Em poucas horas você já aprendeu e com poucos dias praticando você já devia ser capaz de participar de um projeto. Em vez disso, eu quero dar alguns insights que talvez vocês não saibam mesmo se já usam Git faz tempo. Essa história começa uns 12 anos atrás.</p>
<p>(&hellip;)</p>
<p>Lembro bem que era um dia de semana por volta de 2007, eu tava voltando de algum cliente ou algo assim no meu carro e naquela época eu ouvia alguns podcasts e audiobooks porque eu gastava muitas horas no carro indo e voltando do trabalho. E um dos programas que eu ouvia naquela época era o FLOSS Weekly do Leo Laporte, episódio 19 do dia 1o de setembro de 2007. Uma entrevista com Junio Hamano, entrevistado por ninguém menos que Randal Schwartz que era bem conhecido na comunidade Perl. Vou deixar o link desse episódio nas descrições abaixo. Eu mesmo preciso ouvir de novo porque eu só ouvi uma única vez naquele dia.</p>
<p>Pra dar um contexto, nessa época eu tava deixando de ser consultor SAP e programador Java pra experimentar ser freelancer em Ruby on Rails. Eu contei um pouco disso no episódio sobre SAP e sobre Ruby on Rails então dêem uma olhada. Mas o comum naquela época na maioria das empresas era, ou não usar nenhum tipo de controle de versão de código fonte, ou no máximo algum produto comercial Bem porcaria como Microsoft SourceSafe ou Rational ClearCase. Mundo open source ainda não tinha tanta força, muitos projetos ainda usavam CVS, alguns poucos tavam indo pra Subversion. A primeira década do século XXI ainda tava muito atrasado em relação a qualidade de vida pra desenvolvedores.</p>
<p>Eu era um dos rebeldes chatos que ficava insistindo em tentar usar pelo menos Subversion no mundo corporativo, porque eu já tinha cicatrizes demais por conta do SourceSafe. Ninguém merece aquele lixo. Eu sofri com SourceSafe desde o ano 2000 e já tava de saco cheio. E mesmo Subversion não era nenhuma grande maravilha, era mais a filosofia do “menos pior” mesmo. E CVS, quem teve que lidar com essa outra porcaria nos anos 90 lembra o sofrimento. Mundo open source e mundo corporativo estavam bem mal servidos.</p>
<p>A entrevista do podcast era com Junio Hamano que era, e acho que ainda é, o mantenedor do Git. E ele tava justamente explicando que diabos era isso de Git. Em 2007 literalmente ninguém fora dos colaboradores da kernel do Linux tinham muita idéia sobre isso. Como eu tinha essa péssima experiência com CVS, subversion, SourceSafe e tudo mais, quando eu ouvi a explicação fiquei com vontade de experimentar o quanto antes.</p>
<p>Então cheguei em casa e fui logo pesquisar. Obviamente tinha pouca coisa online a respeito. Foi meio um dejavu, foi a mesma sensação quando pesquisei sobre Ruby on Rails 2 anos antes e também não achava ninguém muito interessado. Mas aí esbarrei numa palestra do próprio Linus Torvalds de poucos meses antes. Foi uma tech talk dentro do Google. Tem um monte de coisas do Google que eu pessoalmente não gosto, mas não se pode negar que eles sempre tentam estar um passo à frente nas tendências. E se tem um video sobre Git que eu diria que deveria ser obrigatório assistir é essa tech talk de Maio de 2007 do Torvalds. A seguinte passagem foi o que vendeu Git pra mim:</p>
<p>(Linus xinga CVS e Subversion)
(anybody who disagree is ugly)</p>
<p>Eu Adoro quando o Torvalds não só pisa no calo, mas puxa o martelo e marreta com força. Em resumo, antes do Git a manutenção do projeto da kernel do Linux era feita literalmente com patches e tarballs. Vou voltar pra explicar isso já já. Mas aí eles encontraram a ferramenta comercial BitKeeper que foi um dos primeiros versionadores de código distribuídos e bem feitos. E assim como o GitHub, era uma ferramenta fechada mas que eles deixaram projetos open source usar. Só que o problema de confiar numa ferramenta comercial é que por um tempo eles podem ser legais e deixar usar de grátis, mas um belo dia podem proibir de usar. E foi o que aconteceu, porque alguém tentou fazer engenharia reversa do protocolo deles, se não me engano, e eles não gostaram nada disso e mudaram de idéia.</p>
<p>Daí o Linux ia ficar sem o BitKeeper. Obviamente outras porcarias como SourceSafe jamais foram cogitados como alternativa. E nem voltar pra coisas como CVS. A outra opção era voltar ao procedimento anterior de patches e tarballs. Depois de lançar a kernel 2.6 o Torvalds queria resolver esse problema de uma vez. Ele olhou as opções e pensou assim.</p>
<p>(eu faço melhor em duas semanas e tava certo)</p>
<p>Eu não sou contra falar grande se a pessoa entrega grande. E Torvalds como todo mundo sabe, entrega. E o resultado disso foi o Git, que ele desenvolveu por volta de 2015. Antes de continuar, vale eu voltar rapidamente na história. Pensa assim, um programador iniciante hoje ou um programador nos anos 70 ou 80, não importa. Você tá nos primeiros passos, você quer escrever código. Então você abre um editor de textos qualquer e começa a criar arquivos e vai salvando em algum diretório.</p>
<p>Muito rapidamente você vai cair nos mesmos erros que todo programador sempre caiu não importa em qual época. Você vai escrever em cima do seu código de ontem, salvar e depois descobrir que fez errado e agora perdeu a versão de ontem. Você vai renomear arquivos, sair apagando, e uma hora vai apagar um arquivo que não devia. Quem nunca? Depois de apagar coisas por acidente algumas vezes, você vai começar a duplicar os arquivos antes de editar. Daí vai começar a ficar com versões diferentes do mesmo arquivo no diretório. Eu cansei de fazer isso quando era novo e cansei de ver projetos dos outros cheio de arquivos velhos que não deviam estar lá.</p>
<p>Daí vem o segundo problema, você precisa que outro desenvolvedor trabalhe no mesmo projeto. Uma das maneiras de fazer isso é colocar o diretório do projeto em alguma pasta compartilhada na rede local. Daí acontece de os dois abrirem o mesmo arquivo pra editar. Você edita e salva primeiro. Daí o segundo cara edita a parte dele e salva por cima! E pronto, você acabou de perder o que tava fazendo.</p>
<p>Por isso que muitos produtos comerciais daquela época como SourceSafe ou ClearCase, que eu já xinguei várias vezes, tem como princípio ser uma camada por cima do sistema de arquivos de rede e te dar a opção de bloquear um arquivo ou diretório. Daí antes de abrir um arquivo, primeiro você bloqueia ou faz checkin. Daí se outro programador tentar abrir o mesmo arquivo o sistema vai negar. E ele tem que esperar você terminar, salvar e dar checkout pra desbloquear o arquivo.</p>
<p>Isso ajuda um pouquinho, mas não muito. Eu lembro que no ano 2000 eu trabalhava no Brasil mas na empresa tinha grupos de programadores em Miami, em Buenos Aires e outros lugares da América Latina. Daí um infeliz em Miami bloqueava um arquivo e ia embora sem desbloquear. E eu precisava editar aqui e não conseguia. Daí tinha que ligar pra alguém que fosse administrador do servidor pra desbloquear forçado.</p>
<p>Esse sistema de bloquear e desbloquear era um lixo na prática. Só servia pra casos muito simples. E isso não resolve o problema de alguém ou você mesmo salvar o que não devia e perder o que tinha antes. Pra isso precisava ter versionamento. Hoje em dia os próprios sistemas operacionais e mesmo produtos como Dropbox já oferecem um mínimo de versionamento. Mesmo que você apague o conteúdo de um arquivo e grave, você pode optar em pedir pro sistema fazer tipo um backup da versão anterior. Daí você tem a opção de tentar recuperar algumas versões pra trás. Veja um Time Machine da Apple por exemplo. Hoje em dia a maioria das pessoas tem no mínimo 128GB de HD mas é muito comum ter meio tera ou mais que um terabyte. O computador que eu uso pra editar meus vídeos tem 2 Samsung EVO nvme2 de 3TB cada, sem contar os backups externos.</p>
<p>Mas no ano 2000 era luxo ter 10 GB de HD. Máquina corporativa podia ter menos que isso ainda. E projeto Java naquela época era pesado como é hoje mesmo. Nos anos 90 pra trás, espaço em disco era mais inacessível ainda. O jeito tosco que eu falei acima de duplicar o arquivo antes de editar, pro caso de você precisar da versão anterior é super ineficiente não só porque vai sujando seu diretório de arquivos desnecessários. Mas vira um enorme desperdício de espaço caro em disco. Por isso acho que foi por volta dos anos 70, na época dos UNIX que surgiram duas ferramentas que existem até hoje nos Linux e devia ser ensinado como parte da matéria de algoritmos e estruturas de dados.</p>
<p>A primeira ferramenta se chama Diff que é usado pra tirar a diferença entre dois arquivos. E aqui é um exemplo de onde estudo de algoritmos faz diferença. Se você como iniciante quiser fazer uma ferramenta que abre dois arquivos e tira a diferença em um terceiro arquivo, como você faria? É um problema exponencial que fica lento muito rápido se implementar desse jeito tosco. Mas existe um algoritmo bem conhecido pra encontrar a subsequência comum mais longa entre dois strings. No pior caso a complexidade vai ser de 2 elevado a m, sendo m o tamanho de um dos arquivos. Mas no melhor caso a complexidade cai pra m vezes n, sendo m e n o tamanho dos dois arquivos. Se fizer da forma ingênua vai ficar exponencialmente pior que isso.</p>
<p>Aliás, se vocês estão estudando algoritmos e estruturas de dados na faculdade, recomendo estudar esse algoritmo também. O professor de vocês pode ajudar. Ou procurem vídeos no YouTube mesmo com a explicação passo a passo pra entender como você consegue achar essa subsequência. A partir dessa subsequência, se um conteúdo existe no arquivo original mas não existe na subsequência, podemos marcar com um caracter como &ldquo;menos&rdquo;. Se não existe no arquivo original mas existe na subsequência, podemos marcar com um caracter como &ldquo;mais&rdquo;. Adicione a essa subsequência alguns metadados como em qual linha do arquivo aquela subsequência se localiza e chegamos no que chamamos do formato de um patch.</p>
<p>Então a ferramenta diff que todo Linux tem consegue achar a subsequência comum mais longa entre dois arquivos, adiciona metadados como o número da linha no arquivo e gera esse arquivo de patch. Patch que em inglês é basicamente um remendo. Alguns dizem que essa é a origem do nome Apache, o servidor web. Era A-Patch, por isso Apache.</p>
<p>Enfim, dentro de um arquivo de patch você tem chunks ou hunks de código, que é literalmente nacos de código. Vários nacos de código, ou hunks, formam um patch, que é um remendo. Daí digamos que você queira mandar as modificações que você fez num arquivo pra um amigo seu que tem o mesmo arquivo. Você envia só o arquivo de patch. E no Linux tem a segunda ferramenta que eu falei, que se chama patch e é feito pra aplicar um arquivo de patch no arquivo original, pra ter a versão nova.</p>
<p>Se você já pelo menos andou fuçando um projeto num GitHub da vida, já deve ter visto o formato de patches só que colorizado. Mas é basicamente a mesma coisa. A ferramenta diff nasceu por volta de 1976 então ele é pouca coisa mais velho do que eu &hellip; e graças a isso ficou mais econômico guardar só as modificações feitas sobre o arquivo original em vez de duplicar o arquivo toda vez só por causa de Uma linha modificada. Então economizamos tanto espaço em disco quanto banda de rede já que se fosse transferir as modificações pela rede, antes precisava ir o arquivo modificado inteiro, agora pode ir só o patch que vai ser muito menor. E de novo, lembre-se que hoje você tem banda larga com 200Mb por segundo ou mais, mas antigamente estamos falando de linhas discadas ou redes lentas que trafegavam alguns kilobytes por segundo só.</p>
<p>Agora, se você tiver outro programador que tá colaborando no mesmo projeto, não basta só dar o patch pra ele, ele precisa ter alguma versão do projeto baixado pra poder aplicar esse patch. E como ele baixa o projeto? Pensando de novo nos primórdios da internet, uma forma de fazer isso seria baixar de um servidor de arquivos. Só que eu já expliquei que temos essa limitação de baixa velocidade. Dependendo do tamanho do projeto pode ficar bem lento de baixar.</p>
<p>Pense que antigamente, anos 70 pra trás, disco rígido era uma coisa bem cara. A forma mais comum nos mainframes e minicomputadores era armazenar dados em fitas. Rolos de fitas. E pra gravar em fita você precisava tentar desperdiçar menos espaço quanto possível e daí no fim dos anos 70 nasce o formato de gravação de arquivos em fita ou tape archive, que você conhece hoje como TAR. A estrutura de dados de um TAR é gravar uma série de objetos um atrás do outro. Tar é traduzido literalmente como alcatrão, um poluente de petróleo, mas pense mais como um piche. Então se você tem uma bola de piche onde objetos vão grudando, você tem um tarball.</p>
<p>Daí que vem esse termo que todo mundo de Unix e Linux sempre usa mas você nunca soube o que era. Na prática pense que em vez de você ter 100 arquivos separados, você combine os 100 um atrás do outro num único arquivão. A grosso modo isso é um tar. E pra ser eficiente o comando tar dos Unix e Linux ganhou capacidade de adicionar compressão sem perda a partir dos anos 90.</p>
<p>Aliás, compressão existem dois tipos, lossy e lossless, ou seja, compressão que perde dados e compressão que não perde dados. Exemplos de compressão lossy que perde dados é JPEG pra imagens ou MP3 pra música. Exemplos de compressão que não perde dados é Zip ou 7z. A compressão mais simples que você mesmo pode aprender sozinho e deveria ser ensinado na faculdade na matéria de algoritmos e estruturas de dados é o algoritmo de Huffman pra compressão de texto.</p>
<p>O formato gzip que você vê até hoje em Linux usa o algoritmo deflate que é uma combinação de uma variação do Lempel-Ziv e do Huffman. Aliás, algoritmos de compressão é outra área que os russos dominavam. Se você já usou ou ouviu falar de RAR o nome vem de Roshal Archive, que é o nome do inventor russo Eugene Roshal. O zip e o 7zip vem de outro russo, Igor Pavlov. No fim dos anos 80 um dos compressores mais famosos na época do MS-DOS era o PKZIP que eu usei bastante. Mas nesse caso era do americano Phil Katz. E foi Katz que inventou o deflate dos Unix. Compressão é outro tema complicado de explicar a história, mas até hoje você usa alguma variação disso. Qualquer página na web que seu navegador baixa é entregue comprimido pelos servidores web, normalmente por gzip.</p>
<p>De qualquer forma, essa tangente foi só pra explicar que com as ferramentas que eu descrevi, você já consegue montar um fluxo de trabalho pra um projeto como a kernel do Linux. O Linus Torvalds faz um tarball da versão 1.0 da kernel e coloca num servidor FTP público. Um outro desenvolvedor como um Alan Cox ou Andrew Morton baixa do FTP e começa a desenvolver alguma nova funcionalidade. Quando termina, faz um diff dos arquivos e envia anexado num e-mail de volta pro Linus. No Unix e Linux existem comandos de terminal pra conectar em servidores POP3 ou IMAP hoje em dia, então o Linus consegue automatizar baixar o e-mail, separar o anexo, e usar o comando patch do Linux pra aplicar os patches na versão na sua máquina. Se tudo estiver ok pra ele, ele faz um novo tarball versão 1.1 e coloca no servidor. Se você já fuçou servidores de FTP já deve ter visto diretórios de projetos com tarballs de diversas versões.</p>
<p>Não é um fluxo eficiente em 2020, mas nos anos 90, estamos falando no que havia de mais avançado em fluxo de trabalho distribuído. Então a internet, somado com ferramental Linux como diff, patch, tar, gzip, FTP, e-mail, permitiram um fluxo de trabalho distribuído e o nascimento do mundo open source como conhecemos hoje. Alguma variação disso já existia no mundo Unix nos anos 80 mas foi com o Linux que esse modelo de trabalho começou a ganhar chão.</p>
<p>Mas isso é no mundo pós-internet comercial e Linux. No começo dos anos 70 já havia o conceito de versionamento de código fonte. Em resumo em 1972 havia o SCSS ou Source Code Control System desenvolvido na linguagem SNOBOL na Bell Labs, obviamente, e usado em sistemas como IBM System 370 até o PDP-11. Nos anos 80 foi substituído por uma evolução, que foi o RCS ou Revision Control System. O RCS ainda é mantido até hoje. Finalmente por volta de 1990 surgiu o Concurrent Version System ou CVS pra substituir o RCS.</p>
<p>O modelo sempre foi o que se chama de centralizado, ou cliente-servidor, que era a arquitetura da moda nos anos 80 e 90. Você configurava um servidor onde ficava o projeto. Na máquina de cada desenvolvedor você instala um cliente que consegue se conectar nesse servidor. O servidor era responsável por coisas como autenticar o login de cada desenvolvedor pra dar permissão de acesso ao código. Daí o desenvolvedor podia fazer checkout pra baixar todo o código; fazer checkin pra subir as modificações e de tempos em tempos dar um update pra baixar as últimas modificações do servidor. O servidor ia guardando os deltas de modificação, possível graças à idéia de patches. Novas versões adicionaram coisas como delta compression pra economizar espaço no disco guardando os patches zipados. E novas funcionalidades foram aparecendo como a idéia de fazer branches.</p>
<p>Branch é um conceito simples com uma operação complicada. Fazer um branch, da forma mais grosseira, é duplicar o diretório do projeto. Daí você trabalha num diretório separado. Quando termina gera os patches e aplica no diretório principal e pode apagar o diretório duplicado. É uma forma de trabalhar numa funcionalidade que pode demorar; ou testar correção de bugs, sem atrapalhar o diretório principal. Isso funciona bem se você trabalha sozinho, ou com poucas pessoas, ou com pessoas que estão muito bem coordenadas entre si. Caso contrário isso se torna um puta pesadelo muito rápido. Nos dias de hoje você tem Pull Requests no GitHub que é basicamente um branch do projeto.</p>
<p>(Linux branch true guru)</p>
<p>Pra adiantar essa história, o que foi o Subversion? Subversion de fato nasceu com o conceito de CVS feito direito. Na prática ele adota um fluxo muito parecido com o CVS mas com uma implementação menos puxadinho e um pouco mais estável. O básico funciona igual, checkout, checkin ou commit, update. Mas branches sempre foi o calcanhar de aquiles. O Subversion consertou branches tarde demais. Só na versão 1.5 que saiu em junho de 2008, que ele ganhou a funcionalidade de merge tracking. 8 anos depois da primeira versão. Aí foi muito pouco muito tarde. Em 2008 foi quando o GitHub começou a bombar e aí o resto é história.</p>
<p>Eu já expliquei porque a gente odeia coisas como SourceSafe, porque o conceito de bloquear arquivos pra só uma pessoa usar de cada vez é uma desgraça. Pior ainda pra gerenciar releases, ou fechar versões. Era tão complicado essas porcarias que nos anos 90 e começo dos anos 2000 havia a figura de um gestor de configuração. Um cara pago pra fazer o equivalente a commit e tagear versões. Pensa no absurdo de precisar de alguém tempo integral pra não corromper muito o código fonte.</p>
<p>O outro problema eram as branches. E é a razão principal de porque CVS e Subversion tiveram reputação tão ruim. A gente era meio que obrigado a usar porque não tinha opção melhor. Ninguém usava porque gostava. Faça um exercício: tenta instalar um servidor CSV em qualquer Linux hoje. Você vai ver que é super chato. Eu mesmo perco a paciência e desisto no meio de tão chato que é configurar um CVS. Subversion é um pouco mais fácil de configurar e mesmo assim é um saco.</p>
<p>Um dos grandes defeitos estruturais do CVS é que ele guardava revisões arquivo por arquivo, ele não entendia o conceito de um único arquivão de patch de uma funcionalidade inteira, como um Pull Request hoje por exemplo, que é um conjunto de commits. De forma simplória um commit é nada mais que um arquivo de patch com um cabeçalho com metadados como o autor do commit e data e hora. Mas enfim, era difícil de gerenciar versões no CVS. Cada desenvolvedor precisava trabalhar num branch ou num fork que, pra todos os efeitos e propósitos, é mais ou menos a mesma coisa, uma duplicata do projeto original. Só que pra combinar as modificações de dezenas de desenvolvedores numa única versão, era um cornojob desgraçado, podia levar um dia inteiro ou mais pra conseguir fazer o merge.</p>
<p>Eis que na virada do século surge a BitMover e seu produto BitKeeper. Muita gente pensa que a principal diferença era ser descentralizado. Mas na realidade a principal novidade foi tornar branches, forks e principalmente merges, operações razoavelmente triviais. Um trabalho que podia levar horas ou dias reduzido pra minutos, e com muito menos margem de erros. Por isso que apesar de ser um produto comercial de código fechado, por pragmatismo, o Linus resolveu migrar o desenvolvimento do kernel pro BitKeeper. Isso foi em 2002, mas em 2 ou 3 anos teve o problema que já falei que a BitMover resolveu botar restrições demais ao ponto que o Linus se encheu.</p>
<p>Com o CVS e Subversion, que eram as alternativas open source, muito atrasados em relação ao BitKeeper, as outras alternativas comerciais sendo as porcarias que sempre foram, muitos começaram a pensar em novas soluções. Assim em 2003 surgiu o Darcs e em 2005 surgiu o Mercurial e a própria Canonical que mantém o Ubuntu fez o Bazaar. E também em 2005 o próprio Linus resolveu fazer o Git. No papel todos eles são muito parecidos. Sistemas de versionamento descentralizados com foco em resolver o problema de branches e merges. A diferença é que todos eles tinham problema de performance, menos o Git.</p>
<p>Mercurial e Bazaar eram feitos em Python, então eles não tem como escalar em performance até certo ponto. Darcs se não me engano era feito em Haskell e também era criticado por problemas de performance. O Git, por outro lado, feito pelo Linus sempre foi hiper rápido, porém difícil de usar. Mas em casos como esse, performance e, principalmente segurança, tem prioridade.</p>
<p>Antes desses novos sistemas, a estrutura de dados que ferramentas como Subversion entendiam era uma árvore. Pense diretórios e arquivos. É uma árvore. Um branch era mais ou menos duplicar a árvore. E um merge era comparar duas árvores. Pra ilustrar um problema óbvio. Pense que no diretório original você renomeia um arquivo. E no diretório duplicado você deleta o mesmo arquivo. Agora tenta fazer o merge disso no Subversion ou CVS, você vai tomar um conflito de merge. Isso é o tipo de coisa que um Git faz merge automaticamente e corretamente, BitKeeper e os outros novos também.</p>
<p>Conflitos de merge é o pesadelo de todo desenvolvedor nesses sistemas antigos e incompetentes. Num Git minimamente bem usado você raramente encontra conflitos. E mesmo quando encontra eles costumam ser fáceis de resolver. Hoje em dia trabalhar em branches é super comum, a gente faz isso o tempo todo. Fazer merge também. Idealmente você faz merges com frequência e evita deixar código não mergeado esquecido num branch por semanas ou meses. Mas em sistemas antigos fazer branches custava meio caro, e com merges eram complicados, você evitava ao máximo fazer com frequência. Só que quanto mais você espera também mais caro vai ficando, então era uma faca de dois gumes. Em vez de ter que passar por todo esse pesadelo a maioria simplesmente desistia de fazer branches e todo mundo trabalhava na mesma versão, que em Subversion se chama trunk, em Git é o que você conhece como master.</p>
<p>Por isso antigamente se tinha tanta discussão sobre quem podia ou quem não podia ter acesso de escrever, ou seja acesso de commit no trunk. O Linus explica isso melhor:</p>
<p>(Linus morons commit access)</p>
<p>Agora, por que branches e merges num sistema como Git é tão infinitamente melhor do que num Subversion antes da versão 1.5? Pra simplificar vamos pensar o que você vê hoje em dia num projeto num GitHub da vida, que usa Git por baixo. Aliás, se você não sabia, você não precisa de GitHub pra usar Git, o GitHub é só um facilitador mas ele é opcional. De qualquer forma, se você já fez qualquer tutorial de Git e já tentou usar um pouco deve ter notado alguns termos que a gente usa. Vamos recapitular, você faz git clone pra puxar uma cópia do projeto pra sua máquina. Você vai fazendo modificações, edita um arquivo, cria um novo, renomeia outro e quando termina uma parte do trabalho, faz um git add pra marcar essas alterações e faz um git commit pra empacotar essas modificações. Se você não sabia disso, o git commit por baixo faz algo equivalente ao diff que falei antes. Um commit é o patch de modificações desde o commit anterior.</p>
<p>Eu fiz o episódio de Criptografia básica porque eu precisava ensinar um conceito lá que vou usar agora. Uma das coisas que o Linus queria garantir é que os dados no repositório Git sejam sempre confiáveis, ou seja, que eu possa confiar que o texto que eu baixo dele está correto. Pra garantir isso a próxima coisa depois do tal diff que o git commit faz é passar o patch por um algoritmo de hashing no caso o SHA-1. Esse é aquele numerozão longo e estranho que todo commit tem. É o hash do patch, a assinatura daquele delta. Lembra do conceito de hash? Se eu modificar um bit dentro desse patch o SHA-1 vai mudar completamente e não vai mais bater com o SHA-1 original então eu sei que o patch está corrompido.</p>
<p>Aqui vale mais uma curiosidade. Eu não entendo quando as pessoas me perguntam &ldquo;Akita, que tutorial de Git eu devo estudar?&rdquo; Qual é a dificuldade de ler a documentação no site oficial das coisas como o primeiro passo? Eu sempre achei óbvio ir no site oficial de tudo que eu quero aprender. Se você tivesse ido no site do Git e aberto o link “Book” que é o livro, e lido o capítulo Git Internals, já saberia muito do que estou dizendo aqui. Só pra não perder tempo, eu vou justamente usar um trecho desse capítulo.</p>
<p>Eu disse que não vou fazer um tutorial, mas eu acho interessante demonstrar algumas coisas neste ponto. Eu fico bastante irritado de ver programadores usando ferramentas que geram arquivos e modificam coisas no seu projeto e ele não saber o que está acontecendo. Deixa eu fazer outra tangente. Quando eu aprendi CVS uma coisa que era necessário pra ferramenta rastrear os arquivos é que ele sujava meu projeto inteiro criando um diretório .cvs em todos os subdiretórios do projeto. Era um saco isso. Esse subdiretório começa com ponto porque todo mundo sabe que em Linux um arquivo que começa com ponto significa escondido. Se você mexesse nesses diretórios tinha muita chance de estragar tudo e ter que dar um checkout de novo pra reconstruir certo.</p>
<p>Subversion fazia a mesma coisa, só que agora eram subdiretórios .svn. Até aí tudo bem. Só que em 2003 eu desenvolvia em Windows e tinha projetos .NET que eu precisava usar Visual Studio. Se você usasse alguma coisa como a porcaria do SourceSafe, tecnicamente tudo funcionava integrado. Mas obviamente eu era rebelde e queria usar Subversion pra tudo. Pois bem, me fodi, Visual Studio crasheava quando tentava abrir um projeto vindo do Subversion. Porque em 2003 nem Windows XP, nem Windows Server 2003 entendiam direito arquivos que começam com ponto. Era um dos motivos de porque quase ninguém usava Subversion com projetos de Visual Studio no começo dos anos 2000.</p>
<p>O que eu fiz? A ferramenta que normalmente se usava pra dar checkout de repositórios Subversion se chamava TortoiseSVN. Era uma ferramentinha feita em Visual C++. Então eu fiz um patch pra esse projeto, eu fiz o Tortoise criar esses subdiretórios de rastreamento como underline &ldquo;_svn&rdquo; em vez de ponto &ldquo;.svn&rdquo; e aí o Visual Studio conseguia abrir o diretório agora sem crashear. Pra fazer isso primeiro eu precisava saber que não tinha importância renomear esse diretório, porque ele só é importante pro cliente de subversion, não pro servidor.</p>
<p>Hoje em dia felizmente o Windows já entende arquivos e diretórios que começa com ponto, porque o Git faz a mesma coisa, ele cria um diretório chamado ponto &ldquo;.git&rdquo;. Mas ele é diferente do CVS e Subversion, porque na realidade agora os arquivos e diretórios do projeto que você vê e edita é que não tem importância. O conteúdo de verdade dos arquivos fica no repositório inteiro dentro desse ponto .git e por isso só tem um na raíz e não em todos os subdiretórios.</p>
<p>Entenda. Se você cria um projeto qualquer, digamos de de Node, de C#, tanto faz, e dá git init pra iniciar, depois git add de tudo e o primeiro git commit, agora você pode apagar os arquivos originais, menos o diretório .git. Mesmo se você apagar qualquer arquivo, qualquer um pode ser recuperado com um simples git checkout. Ele vai procurar o arquivo dentro desse .git, sem precisar perguntar nada pra nenhum servidor. É por isso que você consegue ficar mudando de branches no mesmo diretório, o conteúdo inteiro muda. Arquivos somem e reaparecem. Num Mercurial toda nova branch gera um novo diretório ao lado do diretório original do projeto.</p>
<p>Só que se você listar o que tem dentro desse diretório, você não vai achar os arquivos do seu projeto. Em vez disso vai ter alguns subdiretórios meio estranhos e um maior que é o .git / objects. E nele vai achar um monte de arquivos com nomes que parecem hashes de SHA-1. Ficou complicado? Deixa eu ilustrar. Nesta parte eu vou seguir igualzinho a primeira página do capítulo de Git Internals do livro gratuito que está no site oficial, que é uma coisa que você já devia ter feito também. Vamos lá, primeiro criar um novo repositório local de git com o comando git init test.</p>
<p>Agora vamos listar o tal diretório .git / objects. Vazio. Agora vamos usar um comando que você nunca viu o git hash-object. Vou dar echo de uma string e passar pra esse comando. Agora se listar de novo o diretório você vai ver que apareceu um arquivo lá com o nome sendo o SHA-1 desse string. Agora, pra pegar o conteúdo desse arquivo podemos usar outro comando que você nunca viu e nem nunca vai usar o git cat-file. E pronto, é o conteúdo que passamos antes.</p>
<p>O repositório git é um banco de dados de objetos. Vamos dar echo de um string qualquer pra dentro de um arquivo. Isso é uma operação do sistema operacional, nada a ver com git. Agora vamos usar o git pra gerar o hash do conteúdo do arquivo com o git hash-object de novo. Faltou nesse livro apagar o arquivo original antes de dar echo de novo, senão o próximo comando vai falhar. Então vamos apagar e agora dar echo de um novo conteúdo com o mesmo nome de arquivo. Rodamos hash-object e obviamente o SHA-1 tem que ser diferente pra um conteúdo diferente.</p>
<p>Listando os objetos agora temos mais arquivos. Nesse ponto, podemos apagar de novo o arquivo e recuperar o conteúdo que está no arquivo hasheado. E assim sabemos que é possível recuperar conteúdo que você apagou a partir do versionamento do repositório. É assim que comandos como o git checkout funcionam.</p>
<p>Só que até aqui estamos gravando meramente os conteúdos dos arquivos mas não os metadados como o nome do arquivo, estrutura de diretórios ou atributos. Os objetos que criamos até aqui são meros blobs, um punhado de bytes. Pra organizar, precisamos criar esses metadados que também são objetos, mas em vez de tipo blob eles são tipo tree ou árvore. Pra isso precisamos usar uma área intermediária do Git onde é possível manipular esses metadados. Se você já usou o comando git add é nessa área intermediária que o git estaciona suas modificações.</p>
<p>No nosso caso queremos adicionar os metadados na primeira versão do arquivo text.txt que criamos no começo. Então vamos usar outro comando que você nunca viu o git update-index. E esse número 100644 é o mode do arquivo, que se você já usou Linux antes deve saber do comando chmod que é change mode. 644 é um arquivo normal, 755 é um arquivo executável. Ou sendo mais exato, o primeiro número é o modo pro usuário dono do arquivo, o segundo número é o modo pro grupo e o terceiro número é o modo pra qualquer outro usuário. 7 é ler, escrever e executar, 5 é ler e executar, 4 é só leitura e assim por diante. Estude sobre chmod depois pra entender isso melhor. É coisa básica de Linux.</p>
<p>De qualquer forma, nada foi gravado ainda, pra gravar precisamos agora tirar da área estacionada ou stage com o comando git write-tree. E agora criamos um novo objeto de tipo tree com os metadados e podemos usar o git cat-file pra ver. Agora vamos criar um novo objeto de tree só que apontando pra dois outros objetos, um novo arquivo e um que já existe. Primeiro criamos o arquivo new.txt. Agora damos update-index na versão mais recente do test.txt que criamos antes e também um update-index no arquivo new.txt. De novo, estamos trabalhando na área intermediária de stage, então quando terminamos de adicionar os metadados, agora podemos fazer git write-tree pra gerar o objeto de tree com esses metadados. Vamos listar com cat-file e agora ele mostra os dois blobs que esse novo objeto de tree está apontando.</p>
<p>Entenderam o que estamos fazendo? Estamos manipulando o repositório Git local que é basicamente um banco de dados de objetos, criando os objetos e apontando novos objetos de metadados pra eles. Até agora nossa estrutura está mais ou menos como nessa ilustração.</p>
<p>Finalmente, vamos pro terceiro tipo de objeto interessante aqui, os commits. Vamos criar um commit apontando pro último objeto de tree que criamos. Quando tentamos criar um commit o git vai reclamar que eu ainda não configurei email nem nome pra etiquetar no commit. Pra gerar os mesmos SHA-1 do exemplo eu vou usar o email e nome do Scott Chacon que é o autor desse livro e um dos sócios fundadores do GitHub. Mas na sua máquina você deve usar seu próprio nome e e-mail, claro.</p>
<p>Pronto, agora geramos o commit de novo e novamente ele nos devolve um hash. Veja como todo objeto, seja um blob, um tree ou um commit ou qualquer outro tipo sempre gera um SHA-1 que ao mesmo tempo serve como ID desse objeto e também como validador da integridade do dado. Podemos usar o mesmo cat-file pra mostrar esse commit também usando o hash que acabamos de gerar. E se você já usou git antes, esse formato deve ser familiar pra você.</p>
<p>Vamos gerar mais dois objetos de commit, cada um referenciando o commit anterior. Então criamos o segundo commit apontando pro primeiro. E o terceiro apontando pro segundo. E agora, se usarmos o comando git log, vamos ver tudo organizado.</p>
<p>Entenda o que aconteceu até aqui. Usamos comandos de baixo nível do Git que são conhecidos como plumbing, ou encanamento mesmo, como cat-file, write-tree, update-index, read-tree, pra fazer as mesmas coisas que outros dois comandos que você talvez já conheça, o git add e git commit. Add e Commit são comandos conhecidos como Porcelain, de alto nível. Todo tutorial e curso de Git só ensina os comandos Porcelain, mas entenda que por baixo dos panos na realidade esses comandos fazem uso do encanamento, dos comandos plumbing, que você raramente vai usar. O Linus desenvolveu primeiro os comandos plumbing, foi depois que o Junio Hamano entrou como novo mantenedor que ele e outros colaboradores foram melhorando a usabilidade e criando comandos mais simples pra meros mortais.</p>
<p>Agora vamos terminar essa parte mostrando por outra perspectiva. Vamos abrir o console de Ruby puro. Começamos criando uma variável com um string. Agora vamos criar uma variável chamada header que tem como conteúdo a palavra blog e o tamanho da variável anterior. Finalmente vamos criar uma terceira variável chamada store que é o header mais o conteúdo.</p>
<p>Vamos importar a biblioteca de sha1 do Ruby e gerar a versão hexadecimal de 40 caracteres desse store. Agora vamos abrir um outro terminal e usar o primeiro comando plumbing do Git que vimos, o hash-object. E veja que o SHA-1 é exatamente igual. É assim que o Git estrutura o conteúdo e gera o hash. Se você foi atento deve ter notado que usamos o comando git cat-file pra ver o conteúdo do objeto. Mas porque não usamos o comando cat padrão do Linux? Vamos fazer isso no terminal. E veja que apareceu só um monte de sujeira que não dá pra ler. É um binário, não um texto. Mas o que é esse binário?</p>
<p>Voltando pro Ruby, vamos importar outra biblioteca, a zlib pra ter acesso ao método Deflate. Lembram que eu falei de deflate antes? Que é a compressão do gzip? Então vamos aplicar deflate na variável store e esse é o binário comprimido da string original. Parece maior porque é a representação do binário no terminal, mas o conteúdo real é menor. E pra simular o que o git hash-object faz, vamos gravar o arquivo. Primeiro vamos gerar o caminho completo do diretório e arquivo.</p>
<p>De curiosidade você vai notar que quebramos os primeiros dois caracteres desse SHA-1 pra fazer um subdiretório e colocamos o restante como arquivo dentro dele. Se você é um pouco experiente entende porque. Entenda que diretórios tem limite de arquivos. Eu não lembro se eram uns 64 mil arquivos. Pra evitar ficar tudo num diretório só, criando subdiretórios a partir do SHA-1 vai ajudar a balancear os arquivos, agora você pode ter uns 64 mil subdiretórios cada um com uns 64 mil arquivos cada ou seja, aumentamos a capacidade máxima pra uns 4 bilhões de arquivos… deve ser suficiente.</p>
<p>Agora vamos importar a biblioteca fileutils pra criar os subdiretórios caso não existam. No Linux usamos o comando mkdir com o parâmetro traço -p pra isso, por isso o método no fileutils do Ruby se chama mkdir_p. E finalmente gravamos a versão comprimida do conteúdo no novo arquivo. Pra ver se funcionou, podemos sair do console do Ruby e usar o mesmo comando git cat-file pra abrir esse objeto blob que acabamos de criar. E funciona!</p>
<p>Repetindo, quando você clona um projeto do GitHub pra sua máquina, os diretórios e arquivos do projeto que você vê no seu editor, são só um espelho do que está dentro do diretório .git / objects. O banco de dados de verdade são esses objects. A estrutura de diretórios e arquivos pode ser inteira recriada a partir desses objetos. Mais do que isso, perceberam que quando criamos o SHA-1 ele só considera o blob. Os metadados como nome do arquivo e diretório ficam num objeto separado, do tipo tree.</p>
<p>É uma das razões de porque o Git é mais eficiente pra comparar versões diferentes. Lembram quando eu falei da situação de você renomear um arquivo num branch e apagar o arquivo num outro branch e o conflito que isso dá num Subversion ou CVS? No Git esse conflito não existe, o Git rastreia primeiro o conteúdo, depois os metadados.</p>
<p>Mas essa é só a primeira vantagem. E com esse exemplo de Ruby você sabe como as bibliotecas de Git em qualquer outra linguagem se integram com repositórios Git. É isso que permite um GitHub ou GitLab. A estrutura de dados do Git é aberta e razoavelmente simples de trabalhar.</p>
<p>Eu recomendo que vocês leiam o livro no site oficial do Git pra entender como o resto do Git funciona. Mas agora eu posso voltar pro problema principal: branches e merge. Como eu disse antes, imagine que em sistemas antigos tipo CVS um branch é mais ou menos internamente como duplicar o diretório inteiro do projeto. E o merge é fazer diff no diretório duplicado, gerar os patches de cada arquivo e aplicar de volta no diretório original. Parece simples, mas é muito fácil de ver como isso vai fracassar homericamente.</p>
<p>Esse tipo de merge é o que se chama de 2-way. O Git prioriza não a estrutura de diretórios, mas um grafo, mais especificamente um DAG ou um Direct Acyclic Graph. Acíclico porque os nós desse grafo não apontam de volta um pro outro causando um loop infinito. Vocês viram como manualmente criamos um commit novo apontando pro commit anterior. É isso que o comando git commit faz sempre: cria um commit ligado ao anterior, criando uma cadeia. O DAG é esse grafo de commits. Um branch é nada mais, nada menos que um desvio nesse grafo com uma etiqueta com o nome do branch.</p>
<p>A vantagem disso é que todo branch tem a informação do ponto de Origem. A partir de qual commit começa o branch. Um CVS e o Subversion antes da versão 1.5, não tem essa informação. Depois que você faz merge uma vez, fodeu, você não pode mais continuar trabalhando no branch, porque o próximo merge vai ser um inferno na Terra de conflitos. Até aqui deve estar meio confuso pra vocês visualizarem, então vamos fazer um exemplo simples. Na verdade eu adaptei o exemplo de um artigo da revista Dr. Dobbs que eu li em 2013 pra entender isso e ainda é um bom exemplo, então deixo o link nas descrições abaixo.</p>
<p>Pra variar vamos chamar nossos desenvolvedores fictícios de Alice e Bob. Cada um tem uma cópia do projeto. Em um determinado momento precisamos mergear o trabalho dos dois. Você abre o arquivo da Alice e encontra o comando Print hello na linha 30. Daí abre o mesmo arquivo do Bob e vê o comando um pouco diferente, Print bye na mesma linha 30. E agora? Um CVS olha pra esses dois arquivos e tem que interromper o processo de merge porque ele não tem como decidir qual das duas linhas é a válida. Agora a Alice e o Bob precisam ser chamados pra ver se eles lembram o que era pra ser o correto. Isso é um pequeno exemplo, agora imagina isso acontecendo em centenas de linhas de dezenas de arquivos no mesmo projeto.</p>
<p>Isso é o que chamamos de um merge 2-way, ou seja com duas versões sendo comparadas. A diferença no Git é que quando fazemos um branch ele marca a commit de origem do branch. Agora Alice continua trabalhando no branch original master e Bob começa a trabalhar no novo branch, digamos hotfix, que acabamos de criar. Mesma situação, no final precisamos mesclar o trabalho dele e chegamos no mesmo problema de antes na tal linha 30 do arquivo.</p>
<p>A diferença é que o Git pode voltar no passado até o ponto onde o branch foi criado e olhar como era o arquivo original. Agora temos 3 bases de comparação, a versão da Alice, a versão do Bob e a versão original. E se olharmos a versão original vamos descobrir que originalmente a linha 30 tinha o comando Print bye. Então quem modificou o arquivo foi a Alice. E o Git pode escolher automaticamente usar a versão mais recente que é certamente a da Alice. E o processo de merge não dá mais esse conflito e pode seguir automaticamente.</p>
<p>(Linus 22,000 files)</p>
<p>Agora você começa a entender porque o algoritmo de merge 3-way é absurdamente superior a 2-way. Por causa disso o Git consegue eliminar praticamente todo conflito que daria num CVS. Esse foi o ponto que pra mim mais me deixou impressionado em 2007. Mais do que isso, depois que o Git conclui o merge, ele cria um commit de merge. Agora Alice e Bob podem continuar trabalhando cada um em seu respectivo branch e quando forem fazer um novo merge, o novo ancestral de comparação vai ser esse último merge e não o commit de quando o branch foi criado. Num CVS e no Subversion sem o merge tracking da versão 1.5 que eu falei, eles não tem como saber quando aconteceu o último merge e ele sempre vai comparar tudo de um branch contra tudo do outro branch, então se da primeira vez deu um monte de conflito, vai dar tudo de novo no próximo merge e só vai piorando.</p>
<p>== cortado - começo
Outra coisa que parece confundir muita gente até hoje é a diferença entre rebase e merge. A regra é simples, você nunca faz rebase num branch que espelha um branch num repositório oficial, por exemplo na sua conta no GitHub. Por convenção Isso normalmente é o branch chamado de master, que aponta pra remote/origin/master do servidor. Agora, qualquer branch de nova funcionalidade, ou branch experimental que você cria na sua própria máquina, você sempre deve fazer rebase da master nele. E quando terminar você faz merge da sua branch pra master. Essa é a regra: rebase a partir do master pra sua branch e merge da sua branch pro master.</p>
<p>Agora o que diabos é um rebase na prática. Vamos voltar pro exemplo da Alice e do Bob. Pra facilitar digamos que o Bob faz um branch chamado hotfix a partir do commit 5 do branch master onde a Alice tá trabalhando. Agora a Alice continua trabalhando e faz os commits 6 e 7. E o Bob continua trabalhando no branch dele e faz um commit 8. No histórico do Bob o commit 5 pula direto pro commit 8. Mas pro Bob estar atualizado com o que a Alice anda trabalhando, de tempos em tempos ele pode fazer rebase da master</p>
<p>Quando ele faz rebase, o que o Git faz é mover os commits dele uma área temporária, então é como se ele rebobinasse o branch do Bob até onde ele foi criado, que é o commit 5. Daí o Git pega os commits 6 e 7 da Alice e adiciona no branch do Bob. Agora ele tem a sequência mais atualizada dos commits 5, 6 e 7. E finalmente o Git pega os commits que ele moveu pra área temporária, no caso o commit 8, e aplica a partir do 7. Agora o histórico do Bob está limpo e linear, 5, 6, 7 e 8. E quando ele terminar agora ele faz um merge, que vai gerar um commit 9 ligando com o commit 8 do Bob com o 7 da Alice. E pronto, ele pode apagar o branch temporário dele. Recapitulando, rebase é rebobinar seu branch de volta pro começo, aplicar os commits mais novos do branch original, e depois aplicar seus commits de volta por cima. Só isso.
== cortado - fim</p>
<p>Hoje em dia, se você só usou Git e ocasionalmente fez alguma besteira e deu alguns conflitos num merge, é só isso, um pequeno inconveniente, mas antes de ferramentas como Git, era o que eu falei antes, um verdadeiro inferno na Terra. Como anedota, quando eu era consultor Java e SAP, eu estava num cliente com minha equipe. Um dos desenvolvedores amigo meu fazia faculdade a noite. E ele era do tipo que se irritava com as coisas mesmo. Ele tava atrasado e deu um tanto de conflito quando ele deu um update ou commit. Ele tentava resolver os conflitos e não ia. No fim ele saiu atrasado pra faculdade a noite, puto da vida, e bateu o carro. Então, Subversion já fez alguém bater o carro. Não tô exagerando, era assim ruim.</p>
<p>Então, quando eu ouvi o Torvalds xingando o povo do Subversion, eu sabia exatamente do que ele tava falando. Sério, no marketing do Subversion onde eles falavam que era CVS feito direito, também tinha bastante propaganda de como criar branches era uma coisa simples no Subversion. Isso era totalmente irrelevante. De fato, criar branches num CVS era mais custoso. Mas o problema nunca foi criar branches, o problema sempre foi mergear os branches. E isso o Subversion levou 8 anos pra remendar. Depois dessa tech talk do Torvalds ainda levou mais de um ano pra adicionarem o maldito merge tracking. Mas nesse ponto já era tarde demais, eu já tinha abandonado Subversion e comecei a usar Git.</p>
<p>Mas tinha um problema. Ao meu redor, só eu queria usar Git, pra variar. Quando eu deixei de ser consultor SAP e virei freelancer de Ruby on Rails, os primeiros projetos que peguei ainda eram em Subversion. Felizmente tanto Mercurial quanto Git tinham tipo um adaptador. Era possível fazer checkout, baixar o código de um repositório Subversion, converter localmente em um repositório Git e trabalhar normalmente com Git. Depois que eu tivesse terminado de mergear meus branches e estivesse tudo pronto, dava pra dar commit de volta pro repositório Subversion. Então era possível começar a migrar minha forma de trabalhar mesmo se o cliente não quisesse mudar pra Git.</p>
<p>Essa é a outra vantagem de sistemas distribuídos. Num sistema centralizado, qualquer coisa que você precisa fazer, precisa pedir pro servidor. Então se eu estivesse offline, digamos num aeroporto, e eu quisesse fazer um commit pra não perder acidentalmente o que eu tava fazendo, não tinha como, porque em Subversion commit é o equivalente a commit e push do Git. Ele precisa enviar pro servidor. Mas offline, já era, meu fluxo de trabalho era interrompido. Compare com hoje em dia, 2 anos atrás eu realmente fui codando um projeto durante meu vôo pra Las Vegas, fiquei as 8 horas do vôo codando, comitando e fazendo tudo que eu precisava. Quando cheguei no aeroporto em Las Vegas eu conectei meu notebook no 4G e fiz um push pro repositório no meu GitLab.</p>
<p>Aliás, vale explicar outra coisa. Eu já disse antes que GitHub ou GitLab são opcionais pra usar Git. Quando você faz um git init como eu já mostrei, o diretório .git tem tudo que você precisa. Digamos que estamos na situação do aeroporto ou na situação de eu estar em alguma conferência sem internet. Daí um amigo meu se oferece pra me ajudar. Como ele clona o projeto de mim? De várias maneiras. Ele pode se conectar via SSH no meu notebook. Ele pode simplesmente pegar uma cópia do diretório pela rede local. Ou eu posso copiar o repositório num pendrive e dar pra ele clonar.</p>
<p>Muita gente não sabe disso mas quando você faz um git clone de um GitHub, a diferença pra um repositório criado localmente é só algumas linhas no arquivo de config no diretório .git apontando um remote chamado origin pra um endereço na internet, que pode ser um servidor https ou até mesmo um servidor SSH, ou nem precisa ser um servidor, pode ser um outro diretório local na mesma máquina. Isso mesmo, você pode fazer git clone de um diretório. Deixa eu mostrar isso, vamos pegar o repositório que criamos antes chamado test. Agora vamos sair do diretório. Agora podemos fazer git clone desse repositório e chamar o clone de test2. Pronto, clonado. Se olharmos o arquivo de config do test2 vemos uma configuração de remote origin apontando pro diretório em vez do GitHub por exemplo. E como remote é só um texto de configuração, podemos apontar pra onde a gente quiser depois.</p>
<p>Git e outros sistemas distribuídos tem como característica não precisar de um servidor central que controla tudo. Como hoje já tá comprido demais este episódio um dia eu retomo o assunto, mas basta dizer que a forma como todo mundo usa Git via GitHub hoje é só uma das formas de se trabalhar, que provavelmente funciona melhor pra maioria das pessoas, mas não é adequada pro fluxo de trabalho dos desenvolvedores do projeto da kernel do Linux, por exemplo, por isso eles não estão no GitHub apesar de usarem Git. Eu mesmo prefiro que terceiros não tenham acesso ao meu código fonte e nem dos meus clientes, por isso eu mantenho uma instalação privada de GitLab. Tudo funciona igualzinho ao GitHub se não melhor em muitos casos. E mesmo sem GitLab, pra casos menores, bastaria fazer o que eu já fazia dez anos atrás: dar git init num servidor de SSH e todos os meus amigos podem clonar de lá e a gente já pode trabalhar.</p>
<p>Uma das grandes barreiras que o Git enfrentou desde o começo, principalmente comparado com a concorrência, é que ele foi feito pra ser usado num terminal Linux. O Git é um conjunto de programas de linha de comando, que assume que está num ambiente POSIX. Portanto em Windows sempre foi bem chatinho de usar porque o command prompt sempre foi uma grande porcaria e o suporte POSIX sempre foi meia boca. O que uma década não faz? Se você assistiu meu vídeo sobre o Windows Subsystem for Linux, ou WSL, agora o Windows oferece um terminal decente e agora tá super fácil usar programas de linha de comando de Linux nativamente no Windows. Fora isso diversas IDEs e editores de texto incluindo o Visual Studio Code tem suporte decente de Git também. Então mesmo no Windows você consegue usar Git sem grandes problemas hoje.</p>
<p>Em 2008 quando eu fui trabalhar na Locaweb, assim como muitas outras empresas, eles usavam o que? O famigerado e odiado Microsoft SourceSafe. Eu, do meu jeito delicado de chegar chutando a porta, forcei a migração de todos os projetos e códigos fonte da empresa pra ficar num servidor Git. Na época GitHub ainda tava só começando, GitLab não existia, mas havia um projeto open source chamado Gitorious que dava suporte somente a hospedar projetos open source. Como o código era aberto, eu adicionei a funcionalidade de administração e gerenciamento de usuários e permissões. E foi isso que usamos por muito tempo. Depois que eu saí eles migraram pro GitLab que tinha acabado de lançar e é bem melhor.</p>
<p>Eu lembro que a rejeição ao Git era muito grande, especialmente por conta de suporte ruim a usar em Windows pelo command prompt. Eu literalmente fui sentando na mesa de cada desenvolvedor pra ensinar a usar os comandos e fluxos básicos. Foi uma puta dor de cabeça na época, só um louco pra me deixar fazer isso em 2008. Levou meses até todo mundo se acostumar, mas eu acho que valeu a pena.</p>
<p>Por causa de todas as limitações e a filosofia de servidor centralizado, em corporações antiquadas o mais comum ainda é esconder o máximo do código fonte quanto possível. Normalmente desenvolvedores de departamentos diferentes não tem acesso ao código de outros departamentos. Hoje em dia em ambientes modernos isso pode parecer um absurdo, mas esse era o normal. Como era fácil fazer merda, era melhor limitar o acesso o máximo possível.</p>
<p>Além de forçar a migração do SourceSafe pra Gitorious em 2008 eu também injetei o conceito de todo mundo ver tudo, todo código fonte fica aberto a todo funcionário e qualquer um pode criar branches e forks e colaborar via o equivalente a Pull Requests da época. É o modelo que eu acho que é o mais correto até hoje. Esconder código garante que código ruim sobreviva mais do que deveria. Além de criar politicagem desnecessária.</p>
<p>E estamos chegando ao fim deste episódio! Como eu falei no começo do vídeo, o objetivo de hoje não era fazer tutorial. O próprio Linus tem a mesma resposta que eu usaria:</p>
<p>(Linus feeling lucky)</p>
<p>O objetivo hoje era tentar dar um panorama de porque o Git existe, quais eram as alternativas, porque nenhuma outra foi bem sucedida e o que torna o Git tão diferente do resto. O próprio BitKeeper ainda existe e hoje em dia e integra com Git. A Microsoft ainda tem o Team Foundation Server ou TFS que pra todos os efeitos e propósitos é o sucessor espiritual do SourceSafe e eu espero nunca esbarrar em um. E o TFS também se não me engano tem alguma forma de interfacear com repositórios Git. Graças ao GitHub, rapidamente todo mundo migrou pra Git. Hoje em dia ninguém mais questiona.</p>
<p>Se você é da comunidade Ruby on Rails daquela época e se lembrar da primeira grande conferência no Brasil, a Rails Summit Latin America, vai lembrar que um dos palestrantes que eu trouxe foi o Chris Wanstrath, um dos fundadores do GitHub. Eu tinha entrevistado ele pro meu blog no ano anterior e conheci ele, o Tom e o Scott Chacon na Railsconf um ano antes. O Scott que eu também trouxe se não me engano na Rubyconf Brasil de 2010. Hoje todo mundo conhece o GitHub como a compra de mais de 7 bilhões de dólares pela Microsoft. Eu lembro do GitHub na época em que eles estavam codando o GitHub de algum Starbucks. Bons tempos.</p>
<p>Espero que vocês tenham aprendido um pouco mais sobre essa que é provavelmente a ferramenta mais básica pra qualquer desenvolvedor hoje em dia. Minha expectativa de vida como desenvolvedor melhorou substancialmente depois que eu migrei pra Git em 2007. Se tiverem interesse de ver o que eu falava sobre o assunto vou deixar linkado os posts do meu blog que postei naquela época também. Se curtiram o video deixem um joinha, assinem o canal e não deixem de clicar no sininho pra não perder os próximos episódios. Agora eu acho que devo voltar a publicar toda semana se nada der errado, então a gente se vê, até mais!</p>
]]></content:encoded><category>controle-de-versao</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #68 - Entendendo Conceitos Básicos de CRIPTOGRAFIA | Parte 2/2</title><link>https://akitaonrails.github.io/2019/11/26/akitando-68-entendendo-conceitos-basicos-de-criptografia-parte-2-2/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/11/26/akitando-68-entendendo-conceitos-basicos-de-criptografia-parte-2-2/</guid><pubDate>Tue, 26 Nov 2019 11:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/HCHqtpipwu4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIPTION&lt;span class="hx:absolute hx:-mt-20" id="description"&gt;&lt;/span&gt;
&lt;a href="#description" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Errata: em 12:08 eu escrevi &amp;ldquo;Script&amp;rdquo; mas é &amp;ldquo;Scrypt&amp;rdquo; (com &amp;ldquo;y&amp;rdquo;). E eu falei errado. Giga já é bilhão e Tera são trilhões! um Antminer S17 faz mais de 70 TRILHÕES de hashes por segundo!&lt;/p&gt;
&lt;p&gt;Se você não assistiu a Parte 1, eu dei uma introdução à encriptação simétrica, algoritmos de hash como SHA256, HMAC. Se você já sabe essas coisas pode pular direto pra esta Parte 2.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/HCHqtpipwu4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIPTION<span class="hx:absolute hx:-mt-20" id="description"></span>
    <a href="#description" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Errata: em 12:08 eu escrevi &ldquo;Script&rdquo; mas é &ldquo;Scrypt&rdquo; (com &ldquo;y&rdquo;). E eu falei errado. Giga já é bilhão e Tera são trilhões! um Antminer S17 faz mais de 70 TRILHÕES de hashes por segundo!</p>
<p>Se você não assistiu a Parte 1, eu dei uma introdução à encriptação simétrica, algoritmos de hash como SHA256, HMAC. Se você já sabe essas coisas pode pular direto pra esta Parte 2.</p>
<p>Desta vez vou continuar no problema da tabela de usuários roubada por hackers e se você protegeu adequadamente. Passando por uma analogia de como Bitcoins são minerados e como os mineradores tem acelerado esse processo e o que isso tem a ver com algoritmos de derivação de chaves como PBKDF2, Bcrypt e outros, que diferença fazem comparados a algoritmos de hashing comuns como HMAC.</p>
<p>E finalmente vamos ver sobre Diffie-Hellman, RSA, os prós e contras e o que Curvas Elípticas adicionam a essa solução e como tudo isso forma o TLS e SSH modernos.</p>
<p>Seções:</p>
<ul>
<li>01:10 - recapitulando, tabela roubada</li>
<li>02:18 - mineração de Bitcoin</li>
<li>06:03 - PBKDF2</li>
<li>11:44 - Bcrypt, Scrypt, Argon2</li>
<li>16:52 - Base64, CRC, DV</li>
<li>19:37 - full disk encryption + key derivation function</li>
<li>22:55 - Diffie-Hellman</li>
<li>29:52 - RSA</li>
<li>36:16 - Entendendo TLS</li>
<li>41:04 - Curva Elíptica</li>
</ul>
<p>Links:</p>
<ul>
<li>Serious Security: How to store your users’ passwords safely (<a href="https://nakedsecurity.sophos.com/2013/11/20/serious-security-how-to-store-your-users-passwords-safely/"target="_blank" rel="noopener">https://nakedsecurity.sophos.com/2013/11/20/serious-security-how-to-store-your-users-passwords-safely/</a>)</li>
<li>Password Hashing: Scrypt, Bcrypt and ARGON2 (<a href="https://medium.com/@mpreziuso/password-hashing-pbkdf2-scrypt-bcrypt-and-argon2-e25aaf41598e"target="_blank" rel="noopener">https://medium.com/@mpreziuso/password-hashing-pbkdf2-scrypt-bcrypt-and-argon2-e25aaf41598e</a>)</li>
<li>Why Bitcoin mining ASICs won&rsquo;t crack your password (<a href="https://rya.nc/asic-cracking.html"target="_blank" rel="noopener">https://rya.nc/asic-cracking.html</a>)</li>
<li>RSA Encryption (<a href="https://brilliant.org/wiki/rsa-encryption/"target="_blank" rel="noopener">https://brilliant.org/wiki/rsa-encryption/</a>)</li>
<li>How RSA Works With Examples (<a href="http://doctrina.org/How-RSA-Works-With-Examples.html"target="_blank" rel="noopener">http://doctrina.org/How-RSA-Works-With-Examples.html</a>)</li>
<li>Understanding the SSH Encryption and Connection Process (<a href="https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process"target="_blank" rel="noopener">https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process</a>)</li>
<li>SSH-KEYGEN - GENERATE A NEW SSH KEY (<a href="https://www.ssh.com/ssh/keygen/"target="_blank" rel="noopener">https://www.ssh.com/ssh/keygen/</a>)</li>
<li>Upgrade Your SSH Key to Ed25519 (<a href="https://medium.com/risan/upgrade-your-ssh-key-to-ed25519-c6e8d60d3c54"target="_blank" rel="noopener">https://medium.com/risan/upgrade-your-ssh-key-to-ed25519-c6e8d60d3c54</a>)</li>
<li>A (relatively easy to understand) primer on elliptic curve cryptography (<a href="https://arstechnica.com/information-technology/2013/10/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/"target="_blank" rel="noopener">https://arstechnica.com/information-technology/2013/10/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/</a>)</li>
<li>Supersingular isogeny key exchange (<a href="https://en.m.wikipedia.org/wiki/Supersingular_isogeny_key_exchange"target="_blank" rel="noopener">https://en.m.wikipedia.org/wiki/Supersingular_isogeny_key_exchange</a>)</li>
<li>What is TLS and how does it work? (<a href="https://www.comparitech.com/blog/information-security/tls-encryption/#What_does_TLS_do"target="_blank" rel="noopener">https://www.comparitech.com/blog/information-security/tls-encryption/#What_does_TLS_do</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>Pois bem, vamos pra parte 2 do tema de conceitos básicos de criptografia. Semana passada eu vomitei um tanto de informação, e espero que vocês tenham conseguido acompanhar. Esta semana não vai ser diferente. Portanto peguem papel e caneta de novo e preparem-se pra fazer muitas anotações.</p>
<p>Da mesma forma como na semana passada, a matemática é muito pesada, e eu não sou a pessoa mais indicada pra descer nos detalhes das provas matemáticas. Já vou me considerar com sorte se eu não confundi os conceitos durante as explicações. Se tiverem interesse, na internet existem centenas de documentações explicando toda a matemática em detalhes. Vale a pena dar uma olhada, porque se eu fosse tentar explicar em video, teria que fazer um outro canal inteiro do tamanho do Numberphile.</p>
<p>Recapitulando, semana passada falamos sobre encriptação de chave simétrica, falamos sobre algoritmos de hashing e message digest, falamos sobre derivação de chaves. Agora vamos continuar o assunto começando a mostrar as ramificações por cima disso.</p>
<p>Continuando a nossa historinha hipotética da tabela de senhas roubadas. Já estabelecemos que não devemos gravar senhas dos usuários em plaintext numa tabela em nosso banco. Também já estabelecemos que não devem ser encriptadas, e agora tentamos gravar o hash. Mas como já vimos, é fácil atacar com dicionários ou rainbow tables com hashes pré-calculados. E como ainda existe o paradoxo do aniversário que garante que existe uma boa probabilidade de colisões, é possível encontrar uma palavra que gere o mesmo hash da senha do usuário, mesmo que não seja exatamente a mesma senha. Pra evitar esses ataques, uma forma de mitigar é concatenar algum número grande junto com a senha e daí gerar o hash. Então pra cada novo usuário cadastrado, geramos esse número que chamamos de salt, daí fazemos o hash da senha prefixado com esse salt e gravamos o hash resultante no banco, junto com o salt. Daí vai ser mais difícil um ataque com rainbow tables pré-calculadas. Se você já fuçou o código fonte de qualquer sistema de autenticação, vai encontrar esse campo salt ou equivalente.</p>
<p>Então, resta ao hacker tentar calcular os hashes na hora, usando os salts, via força bruta, concatenando palavras de um dicionário com o salt que está na tabela, até achar um hash que tenha colisão com o hash gravado na tabela. Parece difícil, porém a geração de hashes pode ser acelerado. Aqui é mais uma especulação, nem sei se fazem isso. Mas faça de conta. Se você acompanhou a corrida pelo Bitcoin nos últimos anos deve ter ouvido falar que os tais mineradores acabaram com o estoque de GPUs do mercado em 2017. E que mineradores gigantes como a Bitmain criaram ASICs ou Application Specific Integration Circuits. Lembram o que eu falei dos FPGAs? FPGAs são circuitos programáveis via software, é como se eu pudesse “moldar” o hardware do chip como se fosse um chip feito sob medida. ASICs são exatamente chips feitos sob medida direto, direto da fábrica. Existem ASICs feitos exclusivamente pra gerar hashes específicos de mineração, os famosos Antminer S9 que é uma combinação de ASIC, FPGAs e processadores ARM como controladores. Pra dar uma idéia, um CPU topo de linha como o Intel Core i9 9900K tem o que chamamos de hash power, que é a potência de geração de hashes. São uns 478 hashes por segundo.</p>
<p>Parece bastante? Nah, isso é uma porcaria. Uma GPU pode ser usada pra gerar hashes mais rápido ainda. Uma NVIDIA TITAN X, arquitetura Pascal, que é a geração passada, consegue fazer 1.3 gigahashes por segundo, ou seja, 1.3 milhões de hashes por segundo. É ordens de grandeza mais rápido que uma CPU. E eu posso ligar mais de uma GPU no mesmo computador em paralelo. Mas isso também é uma porcaria, uma única unidade do Antminer S9 consegue fazer uns 14 terahashes por segundo, ou seja, 14 bilhões de hashes por segundo. Agora estamos começando a conversar.</p>
<p>Sem entrar muito nos detalhes de como Bitcoin funciona, o que chamamos de proof of work, que é o processo de assinatura de um bloco de Bitcoin é basicamente achar um hash especial desse bloco. Não é um hash qualquer, você precisa ir alterando um número dentro desse bloco que altera o hash final, até achar um hash que começa com um certo número de zeros à esquerda. Mas você lembra que eu falei que algoritmos de hash precisam embutir os princípios de confusão e difusão de Shannon, ou seja, se eu mudar 1 bit na mensagem original, mais da metade dos bits do hash precisam mudar, de forma que não seja fácil achar um relacionamento entre a mensagem original e o hash final. Se eu ficar alterando só um número dentro do tal bloco de transações de Bitcoin e refazer o hash, ele vai ser completamente diferente do hash anterior, de forma que o comportamento é parecido com a geração de um número pseudo aleatório. Então eu preciso ficar alterando esse um número do bloco e regerando hashes até conseguir achar um hash específico que tem a tal sequência de zeros à esquerda. Essa quantidade de zeros é o que chamamos de dificuldade da mineração.</p>
<p>E você imagina que um minerador precisa de muito hashpower, porque minerar é uma competição. Centenas de mineradores estão tentando assinar o mesmo bloco ao mesmo tempo. Quem achar o hash primeiro ganha um prêmio em Bitcoin, uns 12 bitcoins agora em novembro de 2019. Além do prêmio o minerador ganha as taxas de transação acumuladas nesse bloco. Quem perder, não ganha nada. Por isso quanto mais hashpower o minerador tiver, mais chances tem de achar o hash primeiro. E por isso se usa ASICs como o AntMiner e por isso só com GPUs ou CPUs você não tem chances de competir, porque estamos falando de pular de milhões de hashes por segundo com GPUs pra bilhões de hashes por segundo com Antminers.</p>
<p>Mas, não só de mineração de Bitcoins vive geradores de hashes. Imagine você colocar uma versão derivada de Antminer feita exclusivamente gerar hashes normais. Pra tentar achar a colisão via força bruta numa tabela de senhas roubada. Mesmo com salt, um equivalente Antminer pode ficar permutando num dicionário, concatenando cada palavra com o salt e gerar o hash na hora pra comparar. É um processo parecido com mineração de Bitcoins, mas pode ser usado pra tentar quebrar senhas via força bruta, à uma velocidade de bilhões de hashes por segundo por Antminer. E como cada Antminer custa só 3 mil dólares, imagine se eu colocar um data center de derivados de Antminer com o único objetivo de estourar senhas. Especialmente se forem senhas importantes como senhas de banco ou senhas de funcionários do governo americano.</p>
<p>Voltando à nossa história da tabela de usuários roubada, por isso mesmo, só fazer o hash de senhas, mesmo com o recurso de concatenar com salts antes de hashear, ainda oferece o risco iminente de encontrarmos colisões usando ASICs especializados. E agora o problema é outro, quanto mais rápido for a performance de um algoritmo de hashes, mais rápido esse  ASIC hipotético conseguiria processar. Então existe uma outra forma de proteger. E é usando algoritmos de derivação de chaves com capacidade de adicionar dificuldade no processamento. Um dos mais conhecidos hoje é o Password-based Key Derivation Function ou PBKDF2. Como o nome diz ele é uma função que deriva chaves a partir de senhas.</p>
<p>Vamos entender, uma senha normal é normalmente muito fraca. Não importa se você acha que fez ela ser forte trocando “A” por arroba ou “I” por exclamação, essas coisas qualquer atacante já adiciona no algoritmo de força bruta pra quebrar sua senha. Parta sempre do princípio que sua senha é uma bosta. Por isso sistemas de autenticação baseadas em Kerberos, por exemplo, não usam sua senha diretamente, primeiro elas modificam sua senha num processo conhecido como key strengthening ou key stretching, literalmente fortalecimento e esticamento de chaves. Elas não são necessariamente a mesma coisa, mas pra esse vídeo vamos assumir que são. É basicamente o processo de adicionar um salt gigante, algo com mais de 64-bits de comprimento, e aplicar alguma função HMAC de hashes milhares ou centenas de milhares de vezes, um em cima do outro, e adicionar alguma entropia extra como o horário atual, algumas vezes no meio do processo.</p>
<p>Isso tem duas funções, a primeira é gerar uma nova senha mais forte e mais longa que você pode usar no lugar da sua senha curta e fraca. É uma senha derivada da sua, mas que vai resistir a rainbow tables e dificultar colisões. Mais do que isso, algoritmos como o PBKDF2 tem um fator de dificuldade, parecido em conceito com o processo de proof of work que é usado na mineração de Bitcoins. É literalmente quantas vezes você vai rodar o algoritmo de hash como um SHA2 ou HMAC-SHA1. Se você rodar dezenas de milhares de vezes, o algoritmo vai custar meio segundo, talvez até um segundo pra gerar o hash final.</p>
<p>Com o que falei até agora, você já deve estar entendendo porque é desejável um algoritmo custar caro pra gerar o hash. Eu mencionei a possibilidade de usar um similar a Antminer, um ASIC, pra quebrar senhas hasheadas com salts. Eu fico falando similar, porque na prática você não vai usar um Antminer de verdade, porque a lógica do circuito está feita especificamente pra lógica de proof of work de mineração de Bitcoin. Porém, com um FPGA ou desenhando um novo ASIC específico pra SHA256 por exemplo, ou no mínimo usando GPUs, você já está na casa dos milhões de hashes por segundo. Com um novo ASIC você poderia ir pra bilhões de hashes por segundo. Sabendo que o custo de desenvolvimento de hardware decai todo ano, em breve ficará acessível criar equipamentos assim muito barato, na China. Por isso começamos a migrar pra algoritmos de geração de hashes como o PBKDF2 que custa caro pra processar.</p>
<p>Mas essa não é a única solução, é uma corrida entre aumentar a dificuldade do algoritmo versus conseguirem criar hardware mais rápido com mais hashpower. Imagine o seguinte problema. Digamos que no cadastro de usuários do seu sistema, você resolveu se precaver e escolheu usar PBKDF2 com HMAC-SHA1 com um salt de 64-bits e com dificuldade de umas mil rodadas. Isso leva alguns milissegundos pra gerar o hash e é isso que você vai gravar no banco. Com hardware atual, isso levaria milhares de anos pra achar uma colisão. Dê mais 2 ou 5 anos, digamos que consigam fazer esse tal ASIC, barato, que comece a achar colisões. E se agora eu quiser aumentar a dificuldade e migrar todos os hashes pra usar SHA2, com salts de 128-bits e aumentar pra 100 mil rodadas? Eu não tenho como fazer isso, porque obviamente eu também não tenho a senha original do usuário.</p>
<p>Por isso, outra forma de mitigar que algumas aplicações implementam é atualizar o hash do banco quando o usuário se logar com sucesso da próxima vez, porque é só nesse momento que você tem a senha do usuário de novo. Daí pode gerar um novo hash com maior dificuldade e atualizar no banco. O hash de um PBKDF2 é o hash concatenado do salt, do número de iterações da dificuldade, por isso ele é bem comprido e por isso você não precisa criar outro campo na tabela só pra guardar o valor do salt.</p>
<p>Além de aumentar a dificuldade exigindo mais processamento, existem outras formas de mitigar. Por exemplo, o custo de memória RAM costuma ser proporcionalmente maior do que CPUs, ao longo do tempo. Elas diminuem menos rápido de preço. E se criarmos um algoritmo de hash que além de ser caro pra processar também exige um uso de memória bem elevado. E é isso que faz algoritmos como bcrypt, scrypt e outros novos como Argos2. Finalmente chegamos no bcrypt que muitos tutoriais hoje recomendam usar, seja em PHP ou Ruby, você vai achar bibliotecas e tutoriais que usam o bcrypt pra fazer o hash da senha. Assim como o PBKDF2 ele tem dificuldade configurável, então pra produção você deixa uma dificuldade elevada, mas pra sua máquina de desenvolvimento ou pra ambiente de testes com ferramentas de continuous integration, você deixa uma dificuldade baixa pra rodar rápido.</p>
<p>O bcrypt na realidade não é um algoritmo específico de hash, ele pode ser usado como uma função de derivação de chaves através de algoritmo de encriptação simétrica, no caso o cipher Blowfish. O Blowfish foi criado como uma alternativa mais segura e mais rápida que o DES. Lembram que falei dele no episódio passado? Na verdade, acho que qualquer coisa hoje em dia é mais seguro e mais rápido que o DES. O Blowfish tem uma característica importante que nos interessa: ele um custo de setup alto, ele precisa de 4kbytes de memória pra cada novo setup. Parece pouco, mas isso escala rápido e torna o Blowfish caro em termos de uso de memória. Vocês lembram que uma das formas de deixar o DES mais rápido era rodar o algoritmo três vezes por bloco. Pois bem, existe uma derivação do Blowfish chamado TwoFish que foi um dos finalistas na competição da NIST pra escolher quem seria o novo AES. Mas como falei no episódio anterior, quem ganhou foi o Rijndael.</p>
<p>De qualquer maneira, podemos usar o Blowfish pra gerar hashes. Isso é uma coisa que nem todo mundo sabe: hashes podem ser gerados a partir de algoritmos de encriptação de chave simétrica. O bcrypt é um exemplo disso, usando o Blowfish pra gerar hashes. Diferente do PBKDF2 você não precisa gerar e passar um salt porque o bcrypt gera um salt automaticamente, por isso se você rodar o bcrypt várias vezes em cima de uma mensagem, ele vai gerar hashes diferentes toda vez. E assim com o PBKDF2 você pode configurar a dificuldade, o custo dele. Inclusive o parâmetro se chama “cost”. Assim, quanto maior o custo, mais rodadas ele vai fazer e mais demorado vai ficar. Mas diferente do PBKDF2, só com um ASIC você não vai conseguir acelerar um ataque de força bruta porque além de hashpower você vai precisar de muita RAM, elevando o custo, e dificultando alguém tentar algo assim.</p>
<p>Existem outros sucessores ao bcrypt como o scrypt e Argon2, que foram desenhados especificamente pra serem mais caros ainda em uso de memória e dificultar ataques de hashpower via GPUs ou ASICs. Muitos frameworks web hoje usam o bcrypt porque ele é melhor que nada. Mas eu imagino que se alguém precisasse escolher o algoritmo de hash mais resistente e moderno seria ou o Argon2 que é o mais flexivel em termos de customização ou no mínimo o scrypt.</p>
<p>Concluindo a parte de como proteger senhas, sabemos que jamais, nunca, devemos gravar em plaintext. Também sabemos que não adianta encriptar a senha e gravar no banco. Aprendemos também que só usar um hash, seja um SHA256 ou um HMAC-SHA256 por si só, não vai resistir muito tempo a ataques de dicionários ou rainbow tables. Adicionar salts ajuda bastante, mas não vai ser resistente a ataques via hardware especializado como GPUs ou ASICs. Portanto, a melhor medida é usar um algoritmo de derivação de chaves que seja muito caro tanto em processamento quanto uso de memória como Argon2 ou scrypt, e de tempos em tempos, toda vez que um usuário se logar com sucesso, gerar um novo hash com uma dificuldade ainda maior e gravar no banco. Dessa forma mitigamos o máximo possível caso o banco de dados seja comprometido e os dados caiam na mão de hackers dispostos a quebrar suas senhas.</p>
<p>“Mas, meu site usa autenticação de dois fatores, só com a senha ele não vai conseguir entrar.” O grande lance de quebrar senhas não é pra usar só no seu próprio site. Um dos problemas com usuários normais é que eles usam a mesma senha em sites diferentes . Se num banco de dados de 1 milhão de pessoas, eu conseguir 100 mil senhas, são 100 mil possibilidades pra se usar em diversos outros sites que os usuários tenham contas. Por isso mesmo falamos que além de habilitar autenticação de dois fatores, cada conta em cada site que você tenha cadastro deve usar uma senha segura diferente. E por isso você devia estar usando ferramentas de gerenciamento de senhas como 1Password ou LastPass ou equivalentes. Eu faço isso, nenhuma conta minha tem senhas repetidas. Twitter, Facebook, GitHub, Instagram, Google, cada uma usa senhas de uns 40 caracteres aleatórios gerados por ferramenta, impossíveis de decorar.</p>
<p>Uma pequena tangente, eu já falei como muita gente confunde hashes de message digests, funções de derivação de chaves, ou mesmo UUID, GUID, IPv6, porque todos eles são strings com caracteres representando hexadecimais que parecem aleatórios. Existe ainda outro gerador que cospe strings que parecem bagunçadas que podem confundir, esse é o Base64. Base64 é uma ferramenta usada pra converter um binário em strings de texto. Cada caractere do base64 representa 6-bits, então três bytes de 8-bits são representados em 4 caracteres de 6-bits. Portanto sempre se lembre que converter um binário em texto via Base64 aumenta o tamanho final em pelo menos 20% se não forem comprimidos com gzip ou algo assim.</p>
<p>E porque eu preciso converter binários em texto? Simples, porque todo protocolo de internet como os de e-mail e mesmo HTTP são protocolos texto. Se eu quiser enviar um e-mail com um anexo que é um arquivo PDF ou word ou uma imagem, ele precisa converter em texto pra poder concatenar no corpo do e-mail. Sim, se você não sabia disso, todo e-mail é sempre 100% texto e todo binário precisa ser convertido em base64 pra ser anexado e por isso todo anexo de e-mail é mais pesado que o binário original, e por isso você deve evitar mandar anexos gigantes.</p>
<p>Eu acho que não preciso dizer isso a esta altura, mas eu fico pasmo toda vez que vejo um programador que acha que Base64 é uma forma de encriptação. Ela não é. Ela é só uma conversão simples de binário pra texto. Sem chave, sem entropia, sem nenhuma operação que bagunça seu conteúdo. Nunca use Base64 achando que está adicionando alguma dificuldade de obfuscação, pois não está. Base64 é trivialmente reversível do texto pro binário. Outro nome que talvez você veja por aí é ASCII armor ou Radix-64, mas é tudo Base64. A mesma coisa vale pra checksums ou CRC que é cyclic redundancy check, em resumo é um cálculo feito em cima de um número pra ver se algum dos dígitos está errado. Por exemplo, um código de barras de um boleto bancário tem um CRC, dessa forma é muito fácil checar se a pessoa digitou um número errado do boleto. Naquele códigozão que você digita no seu internet banking tem um CRC no final, que é um número calculado a partir dos anteriores. Digite um número errado e o CRC não vai bater. CRC também é útil pra checagem de erros no tráfego de pacotes de rede. O protocolo TCP tem uma checagem de CRC meio fraco, e por isso existe outra checagem na camada 2, no nível do frame de Ethernet por exemplo. É uma forma de rapidamente checar errors de transmissão causado por barulho ou interferência na rede por exemplo. Mas isso também não tem nada a ver com hashing ou encriptação.</p>
<p>Até agora tudo que falamos de encriptação depende de um componente: um segredo, ou uma chave. Pra coisas como encriptar seu HD, isso é suficiente. Quando você usa o BitLocker do Windows 10 Pro ou  o FileVault do MacOS ou o LUKS no Linux, eles vão fazer um processo similar. Você começa providenciando uma senha que deveria ser forte. Mas como ao mesmo tempo você não quer esquecer a senha e perder acesso aos seus dados, provavelmente você vai acabar usando uma senha porcaria. Mas não tem problema, o sistema de encriptação vai gerar uma senha forte pra você, usando um salt gigante com um algoritmo de derivação de chave como o Scrypt ou PBKDF2 que, como já expliquei, são resistentes a ataques de força bruta ou dicionários e até ASICS. Aqui eu estou chutando, mas essa ainda não deve ser o segredo pra encriptação. Ele vai gerar outra senha forte, que seria o mount key. Esse mount key é que vai ser usado pra encriptar os dados do seu disco. E a senha forte derivada da sua vai ser usada pra encriptar o mount key.</p>
<p>Dessa forma, se amanhã você quiser mudar sua senha ou passphrase, você vai só mudar a derivação que abre seu mount key. E não precisaria decriptar e encriptar seu disco inteiro tudo de novo. Adicione ainda um token ou identificação biométrica como autenticação de dois fatores e você tem um pouco mais de segurança. Entenda que além dos seus dados, tem outra coisa importante que precisa ficar encriptado: o seu swap. O swap é como se fosse uma extensão da RAM em disco. Não dá pra saber o que tem nesse swap. Se alguém pegar seu swap lá pode ter o que tinha na memória das aplicações que estavam rodando na máquina. Números de cartão de crédito, mesmo senhas, ou outros dados sensitivos podem estar lá. Você gostaria que a memória também esteja encriptada.</p>
<p>A Intel, por exemplo, tem o SGX ou Safe Guard Extensions, em resumo ele garante que dados específicos que saem do processador pra memória já saiam direto do processador encriptados, colocados numa região específica da memória que chamamos de “enclaves” ou secure enclaves. Pense numa partição criptografada dentro da RAM que só é lida e escrita pela CPU e nem seu sistema operacional tem acesso, nem como root, nem como administrador nem nada. Não é 100% do que está na RAM mas só coisas específicas que você manda via uma API. É uma API que pode ser usada por softwares que precisem trabalhar com dados sensíveis como sua chave mestra, de forma que ela não fique nem por um milissegundo exposta na RAM em plaintext e suscetível a ser roubada dali por um malware.</p>
<p>Resumindo até agora. Falamos de algoritmos de hashing, tanto obsoletos como MD5 e SHA1 quanto mais novos como HMAC-SHA256. Falamos de derivação de chaves como Bcrypt e PBKDF2. De encriptação simétrica de chave. Coisas como o AES Rijndael ou Blowfish ou TripleDES que são todos algoritmos block cipher, por processarem um bloco de cada vez. Mas existem outros algoritmos como RC4 que são stream ciphers, que aplica processamento bit a bit em vez de bloco a bloco. Mas pros propósitos de hoje, só block ciphers é suficiente. Falta falarmos de encriptação assimétrica e troca de chaves.</p>
<p>Tudo que falamos até agora de encriptação depende de uma chave secreta, é a chave pra encriptar e decriptar. Dentro da mesma máquina tudo funciona. Mas e quando queremos mandar um ciphertext pra outra pessoa? Pra essa outra pessoa decriptar a mensagem ele precisa ter o mesmo segredo usado pra encriptar. Só que não existe nada mais inseguro do que trafegar uma senha pela internet. Essa senha vai ser certamente interceptada no meio do caminho e toda sua segurança vai por água abaixo. Queremos um jeito de criar uma chave na minha máquina e na máquina de outra pessoa, sem trafegar as chaves, de tal forma que o que eu encriptar com a chave que só eu tenha seja possível de ser decriptada com a chave que só a outra pessoa tenha. Esse é o conceito.</p>
<p>Pra simplificar vamos falar das duas mais importantes. Diffie-Hellman e RSA. Ambas cujos nomes são os nomes dos criadores, no caso Whitfield Diffie e Martin Hellman e no caso de RSA é Rivest, Shamir e Adleman. Vocês devem ter percebido que muitos algoritmos de segurança ganham uma derivação do nome dos criadores, mesma coisa com Rijndael que é o AES, por exemplo. E tanto Diffie-Hellman e RSA por coincidência tem minha idade, já que nasceram em 1977, o ano onde descobrimos que é possível trafegar comunicação encriptada sem o risco de trafegar uma chave secreta e eu argumentaria que é a partir desse ponto que a internet realmente se torna comercialmente viável e anos depois permitiria coisas como e-commerce.</p>
<p>Já avisando que felizmente é fácil explicar como esses algoritmos funcionam, mas vai ser impossível demonstrar “porque” eles realmente funcionam, falta matemática pra explicar aritmética modular, teorema de Euler, totientes e muito mais, então de novo, por hoje vocês vão ter que acreditar no que eu disser, mas eu recomendo que você pesquisem as provas matemáticas. Foi isso que diferenciou Diffie-Hellman, porque eles conseguiram demonstrar matematicamente a segurança do algoritmo.</p>
<p>Pra que serve Diffie-Hellman? Ele possibilita que duas pessoas consigam gerar duas chaves, separadamente, cada um gerando sua própria senha em sua própria máquina, sem nunca trafegar essas chaves. E eles garantem que, o que a chave de um encriptar, a chave do outro consegue decriptar. Conseguem entender as implicações? Todo mundo explica Diffie-Hellman usando cores ou números pequenos pra facilitar a explicação, então vou roubar a explicação da Wikipedia porque é bem simples.</p>
<p>Imagine duas pessoas na internet, Alice e Bob. Primeiro eles trocam duas informações públicas, de modo inseguro via internet mesmo, um número é o módulo p igual a 23, por exemplo, e uma base g igual a 5, e 5 porque ela é uma raíz primitiva módulo 23. Falei que precisa de aritmética modular. Daí Alice escolhe um segredo chamado azinho que ela não divulga pra ninguém, um número como 4. Agora vai complicar um pouco. Alice precisa calcular o g, que é um número público, elevado a esse azinho, que é secreto, módulo p, ou seja, 5 elevado a 4 módulo 23 que por acaso é 4 de novo, porque estamos usando números muito pequenos. Esse resultado 4, que vamos chamar de Azão, ela pode mandar publicamente pro Bob via internet insegura.</p>
<p>Agora o Bob também escolhe um segredo que ele não divulga, um bezinho igual a 3, e faz a mesma conta de g elevado a bezinho módulo p, ou seja, 5 elevado a 3 módulo 23 que é 10. Vamos chamar isso de Bezão e Bob pode mandar esse Bezão pra Alice via internet também, aberto. Alice agora pode computar o Bezão do Bob elevado ao azinho dela módulo p. Ou seja, 10 elevado a 4 módulo 23. Isso vai dar 18. E o Bob pode fazer o mesmo, pegar o Azão que recebeu da Alice, elevar com o segredo bezinho dele que é 3, e fazer módulo 23 e o resultado é 18 também. Pronto, tanto Alice quanto Bob compartilham o mesmo segredo 18 e esse número nunca trafegou pela internet, e mesmo sabendo o módulo 23, a base 5, o Azão 4 da Alice e o Bzão 10 do Bob é impossível chegar no segredo 18 que só os dois conseguem ter.</p>
<p>Uma pessoa que ouviu a comunicação no meio do caminho não tem como calcular o segredo. Essa é a propriedade de grupo multiplicativo de inteiros módulo p. Agora podemos fazer um derivação de chave desse segredo - ou no caso de um TLS/SSL eles fazem acho que só um hash simples via SHA2, porque acho que TLS antecede a criação do conceito de derivação de chaves. E esse hash pode ser usado como o segredo por ambos num algoritmo de encriptação de chave simétrica, como o AES, e trafegar mensagens encriptadas inviáveis de decriptar, como já expliquei antes sobre o AES. Relembrando que a idéia de fazer uma derivação de chaves via hashes seria pra esticar e fortalecer a chave.</p>
<p>Claro, esse exemplo usa inteiros miseravelmente pequenos, na realidade o tal módulo p vai ser um número primo gigantesco, digamos de 4096-bits. A dificuldade pra alguém de fora é descobrir qual número que podemos elevar o Azão ou o Bezão módulo p que vai nos dar o segredo. E pra achar esse número caímos no problema de logarítmo discreto, que é computacionalmente inviável de resolver, e esse é o problema que só um algoritmo quântico de Shor conseguiria tentar resolver, num computador quântico de mais de 4000 qubits, como eu já expliquei no episódio anterior.</p>
<p>Diffie-Hellman é bom pra criar chaves temporárias. Mas tem um problema, ele sozinho não tem como autenticar as identidades da Alice e do Bob. Por exemplo, uma terceira pessoa chamada Eve podia fazer um ataque man-in-the-middle, ou seja, toda comunicação da Alice passa pela Eve e ela devolve pro Bob e vice versa. Daí a Alice e a Eve acabam combinando o segredo via Diffie-Hellman e depois a Eve e o Bob combinam outro segredo. E pronto, ela participando desse processo vai produzir segredos conhecidos com os dois e consegue ao mesmo tempo ver a comunicação e repassar a comunicação entre os dois, de tal forma que Alice e Bob não percebam que a comunicação está sendo interceptada e repassada.</p>
<p>Pra evitar esse ataque de man-in-the-middle existe outro protocolo usado por cima do Diffie-Hellman que é o STS ou protocolo Station-to-Station que em resumo exige uma checagem de assinaturas entre os participantes, pra validar as identidades, antes de entrar no processo de geração de chaves secretas. E isso envolve o uso de certificações de chave pública, onde tanto Bob quanto Alice tem um par de chaves privadas e chaves públicas. Isso chamamos de chaves assimétricas.</p>
<p>Pra entender chaves assimétricas, vamos de uma vez entrar no RSA. Aqui a matemática fica mais difícil. Se você já gerou chaves públicas pra cadastrar no GitHub provavelmente teve que usar o comando ssh-keygen e normalmente o padrão é usar a opção RSA. Em resumo hiper resumido, o processo começa escolhendo dois números primos gigantescos, de novo lá pela casa dos 2048 bits. Na verdade os tutoriais todos devem estar recomendando você escolher 4096 bits. O primeiro processo envolve a multiplicação desses dois primos, esse é o módulo da operação. Daí você escolhe um outro número coprimo da função totiente de Carmichael pra esse módulo, esse vai ser o expoente da operação.</p>
<p>A chave pública é a combinação do módulo e do expoente. Agora você acha o inverso multiplicativo modular do expoente inverso módulo esse totiente. Essa é a chave privada. Nem se preocupe em saber o que é função totiente ou inverso multiplicativo modular. Se quiser entender toda a matemática vai precisar saber o que é o algoritmo de extensão euclidiano, teorema de Lagrange, e de novo o tal grupo multiplicativo de inteiros módulo p que usamos em Diffie-Hellman. Mas entenda só que a operação inicia com dois números primos gigantes, com ele você calcula a chave pública primeiro e dela deriva a chave privada, e por isso elas são relacionadas.</p>
<p>A propriedade mais importante é que tudo que você encriptar com a chave pública só pode ser decriptada com a chave privada, e vice-versa. A chave privada você guarda num diretório na sua máquina, com o máximo de permissões fechadas, tipo o que você tem no diretório .ssh em todo Linux. E a chave pública é a que você pode divulgar pra qualquer um e é o que você cadastra na sua conta no GitHub por exemplo.</p>
<p>Mas como Alice e Bob conseguem autenticar a identidade deles? Ou seja, como Alice que sabe que Bob é confiável e vice versa? Aqui entra o papel de um Central Authority ou CA, como uma Certisign. Todo sistema operacional vem pré-instalado com as chaves públicas de autoridades confiáveis como a Certisign, Sectigo, Digicert, GlobalSign e outros. Quando você clica no cadeado do seu navegador e for no caminho da certificação, você vai ver no topo um desses CAs. Você confia neles porque vieram de alguém confiável. O Windows veio da Microsoft, a Microsoft confia nessas CAs e seu navegador vai confiar nelas também. Confiança é uma cadeia. Pra duas pontas desconhecidas confiarem uma nas outras elas precisam de um terceiro que ambos confiam primeiro. Esse é o princípio.</p>
<p>A Alice começa mandando antecipadamente a chave pública dela pra uma GlobalSign por exemplo. A GlobalSign vai usar a chave privada dela pra assinar a chave pública da Alice. A chave e a assinatura são armazenados num pacote especial, tipo um template, que é o formato X.509. De novo, quando você clica no cadeado em algum site com TLS ele abre várias informações, e é isso que vem dentro de um certificado X.509. Mais importante, nesse certificado está a chave da Alice assinada pela GlobalSign. Mas o que é essa assinatura?</p>
<p>Aqui vou simplificar de novo. Pra assinar alguma coisa com uma das chaves considere que as chaves são números. Digamos que a chave privada seja o número 10 e a chave pública seja o número 4. Você pega caractere a caractere da mensagem que quer assinar e multiplica ela por ela mesma 10 vezes. Se o caractere em binário é o número 2, vamos fazer 2 vezes 2 vezes 2 … 10 vezes. Lembram que no começo determinamos um módulo? Então se o resultado dessa multiplicação for maior que o módulo, aplicamos o módulo nele e essa é a mensagem encriptada. Agora pra decriptar pegamos essa mensagem e de novo, caractere a caractere multiplicamos ela por ela mesmo 4 vezes, que é a chave pública do nosso exemplo. Repetindo, se o número for maior que o módulo, aplicamos o módulo no número e o resultado vai ser a mensagem decriptada. Esse é o poder da aritmética modular.</p>
<p>Quantas vezes vamos precisar multiplicar cada caractere da mensagem? Lembram que eu falei que a gente gera chaves de 4096 bits? Isso é 2 elevado a 4096 que vai dar um número da ordem de 1233 dígitos. É um número astronômico e nenhuma calculadora nem sua linguagem favorita vai conseguir multiplicar. Mas calma, não vamos usar esse numerozão não. Sempre temos um módulo n, lembram? Vai ser o numerozão módulo n. Ainda assim é um numerozão, mas ordens de grandeza menor. E vamos multiplicar cada caractere da mensagem elevado a esse numerozão módulo n.</p>
<p>Voltando ao exemplo, é assim que a GlobalSign usa a chave privada dela pra assinar a chave pública da Alice, e ela coloca o resultado, que é a assinatura, num certificado X.509 que a Alice pode agora mandar pro Bob. O Bob confia na GlobalSign e tem a chave pública dela pré-instalada na sua máquina. Com isso a chave pública da GlobalSign ele pode decriptar a assinatura do certificado e achar a chave pública da Alice dentro. Com isso o Bob pode confiar na Alice. Agora o Bob pode enviar mensagens com segurança pra Alice. Pra isso ele pode usar a chave pública da Alice pra encriptar uma senha secreta e só a Alice vai poder decriptar com a chave privada que só ela tem. Pronto, como em Diffie-Hellman ambos os participantes possuem a mesma chave simétrica pra iniciar uma conversa segura encriptada. Podemos usar um AES pra trafegar mensagens encriptadas.</p>
<p>De tudo isso temos alguns problemas. No caso do Diffie-Hellman sozinho, ele não tem esse mecanismo de CAs e um terceiro de confiança pra assinar as chaves públicas. Portanto ela não tem como garantir a identidade dos participantes. Com STS ou no caso o RSA que é mais usado, ambos os participantes tem chaves pré-criadas e pré-assinadas, e eles podem fazer esse processo de aperto de mão, ou handshake, pra validar a identidade. Se o Bob consegue abrir a identidade da Alice com a chave pública da GlobalSign, sabemos que Bob confia na GlobalSign e a GlobalSign confia na Alice então, por consequência, o Bob pode confiar na Alice, e agora temos autenticação comprovada.</p>
<p>Mas porque usar AES pra encriptar a comunicação em vez de só usar RSA direto pra fazer a encriptação de tudo? Porque RSA é computacionalmente muito caro. As chaves são enormes, de 4096 bits, e tem esse processo ridiculamente pesado de multiplicação caractere a caractere pelo módulo da chave de 4096 bits. Seria inviável encriptar um stream de Netflix via RSA, por isso trocamos por AES, que sabemos que é rápido e até acelerado via hardware na CPU. Além disso temos outro problema, forward secrecy. Digamos que no futuro alguém de fato construa um computador quântico funcional de mais de 4000 qubits e execute o algoritmo de Shor. Agora toda comunicação do passado que usou as mesmas chaves assimétricas estão comprometidas. As chaves são a identidade das pessoas e elas raramente mudam. Então tudo que foi encriptado nelas poderia ser decriptado no futuro, por isso falamos que RSA não tem forward secrecy.</p>
<p>Mas Diffie-Hellman tem, porque a idéia é gerar novas chaves secretas a cada sessão. Então o que temos aqui? E se usarmos RSA pra gerar chaves assimétricas, certificados X.509 pra ter as chaves assinadas por uma CA que todos confiam? Com isso podemos autenticar a identidade dos participantes. Daí podemos usar Diffie-Hellman pra trocar segredos que nunca trafegam. E finalmente podemos usar AES pra fechar uma conexão segura e rápida. Em resumo bem resumido é isso que é um TLS ou o que chamamos de SSL faz! E em outras combinações é o que um SSH também faz.</p>
<p>No caso do SSH você usa a ferramenta ssh-keygen pra gerar o par de chaves, daí copia sua chave pública pro arquivo authorized_keys no servidor. Agora quando tentar se conectar no servidor ele pode autenticar sua identidade. No caso mais simples não precisa de um CA porque se a chave pública tá no servidor, alguém de confiança teve que colocar lá e isso já é suficiente pra servir de autenticação. É como funciona no caso do GitHub também.</p>
<p>Tanto no caso de TLS quanto SSH existe uma etapa que é a negociação dos ciphers. Eles não usam só um cipher. Ah é, acho que eu não disse mas chamamos esses algoritmos de criptografia de ciphers. Quando eles fazem o handshake o servidor envia uma lista de combinações de ciphers que ele suporta, o navegador vê sua própria lista e eles escolhem uma combinação que ambos suportam. Isso é um Cipher Suite. As combinações da lista são como esse TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384. Puta monte de letrinha, mas agora acho que você já reconhece quase tudo. TLS é o protocolo. ECDHE é Elliptic Curve Diffie-Hellman Ephemeral. Diffie-Hellman você já sabe, Elliptic Curve ou falar já já. RSA é o sistema autenticação com chaves assimétricas que acabei de falar. AES 256 é como a encriptação vai ser feita. GCM é o modo de execução do AES, chamado Galois Counter Mode, que também vou explicar já já. Finalmente SHA384 é o algoritmo de hashing HMAC que vai ser usado pra verificação de integridade dos dados.</p>
<p>Sobre GCM, você precisa entender que todo algoritmo de encriptação de chave simétrica tem customizações pra serem mais flexíveis. Por exemplo, um erro ao usar TripleDES é usar ele em modo ECB, que é o padrão. Nesse modo o initialization vector, ou IV é nulo e se eu rodar a mesma mensagem pelo TripleDES ele sempre vai gerar o mesmo valor encriptado. Mas adicionando um IV na operação do primeiro bloco vai acontecer uma avalanche porque o bloco seguinte usa o resultado do bloco anterior, como eu já expliquei no ataque de extensão de comprimento. Daí o resultado encriptado vai ser bem diferente pra toda vez que você mudar o IV. É meio como a idéia de um salt, quando geramos hashes pra gravar no banco, como também expliquei antes. Usar o IV é o modo chamado de CBC, que é o recomendado no TripleDES.</p>
<p>O AES também tem modo CBC. Sem entrar em muitos detalhes o modo CBC tem esse requisito de um bloco anterior influenciar o bloco seguinte, em cadeia. Por isso não tem como rodar em paralelo. Já o modo GCM é um tipo de contador e a característica é que você pode encriptar blocos em paralelo, tornando o processo mais rápido. Além disso, o GCM tem características que tornam a encriptação mais segura também, daí você pode usar uma chave menor do que em CBC, ou seja, em vez de usar chaves de 256-bits, pode usar uma de 128 e ter relativamente o mesmo nível de segurança. Você já deve ter entendido que quanto menor a chave, menos trabalho os algoritmos tem, são ordens de grandeza menos multiplicações. E em comunicação via TLS em sites seguros, você gostaria que a comunicação fosse segura mas também fosse rápida, por isso existe a opção de escolher AES GCM.</p>
<p>Agora, EC ou Elliptic Curve. Em resumo resumido, tamos falando literalmente de uma função que gera um gráfico parecido com este aqui do lado, isso é uma curva elíptica. Tem bastante matemática pra entender as propriedades dessa curva, mas uma importante é que se escolhermos dois pontos quaisquer dessa curva e traçarmos uma linha reta, ela sempre vai interseccionar num terceiro ponto. Um dos pontos da linha que escolhemos vai ser a chave privada e o ponto de interseção vai ser a chave pública. Sim, curvas elípticas são usados pra gerar chaves assimétricas. Lembram que no caso de RSA o processo começa escolhendo dois números primos gigantes e depois faz várias derivações pra chegar na chave privada? Aqui, de forma simplificada, é traçar uma linha em cima do gráfico e temos nossas chaves.</p>
<p>A propriedade importante é que as chaves geradas através de curva elíptica são mais resistentes a tentativas de fatorar os primos com Quadratic Siege e General Number Field Sieve. Elas são técnicas matemáticas pra tentar fatorar o numerozão nos primos correspondentes, sem recorrer só a força bruta. Mesmo sem computador quântico e Shor existem outras tentativas matemáticas pra tentar chegar na fatoração de primos. Não é prático e muito menos rápido, então acalmem-se, seus porns e nudes ainda estão seguros. A geração de chaves via curvas elípticas cria chaves ainda mais fortes do que o RSA normal consegue criar, graças à essas propriedades da curva.</p>
<p>Portanto ECDHE, o que é isso? Elliptic Curve Diffie Hellman Ephemeral. Lembram que eu falei que só Diffie Hellman sozinho pode sofrer ataque de man-in-the-middle? E precisamos usar algo como STS? No caso, a parte do Elliptic Curve substitui a necessidade de STS e providencia autenticação ao Diffie Hellman. E lembram como eu falei que as chaves do RSA sofrem de forward secrecy porque as chaves nunca mudam? Por isso tem o Ephemeral no nome desse, porque eles geram chaves assimétrica pra cada sessão e depois elas são descartadas. Assim, mesmo se um dia alguém quebrar as chaves que estou usando na minha sessão de Pornhub agora, o atacante não vai conseguir quebrar as sessões do passado, que usaram chaves efêmeras. Agora combinamos Diffie Hellman com chaves efêmeras via curva elíptica pra trocar segredos e podemos combinar com RSA pra garantir a identidade. E continuamos usando AES pra encriptar os dados em si com as chaves trocadas com Diffie Hellman. Agora sim, esse é mais ou menos o TLS, que você pode conhecer como SSL que todo site e aplicativo usa hoje em dia.</p>
<p>Chaves geradas através de curvas elípticas estão sendo adotadas cada vez mais. É isso que é usado no algoritmo do Bitcoin pra gerar suas chaves. A Apple usa no iMessage. Empresas como DNSCurve usam no DNS. E como vimos na cipher suite de navegadores, eles também suportam. E mesmo em SSH hoje recomenda-se criar chaves com ed25519. Aliás ele tem esse nome porque usa a curva 25519 que é uma curva elíptica. Se você viu essa recomendação em algum tutorial talvez não tenha entendido até hoje porque mandam usar ed25519 em vez de RSA, e porque o tamanho da chave é menor de uns 256 bits em vez dos 4096 bits que se recomenda pra RSA. Agora você já deve ter entendido. Como falei antes, as chaves de curva elíptica são mais fortes, e por isso podem ser menores. E sendo menor você também já sabe: muito menos multiplicações, o que significa uma encriptação mais rápida. Com o aumento na adoção na maioria dos lugares, hoje em dia, se for gerar chaves com ssh-keygen, escolha ed25519 em vez de RSA. O GitHub, por exemplo, suporta. Você vai notar como a chave pública que ele gera é bem mais compacta, justamente porque a chave é menor.</p>
<p>E agora você consegue entender todas as letrinhas que aparecem nos cipher suites dos navegadores. De qualquer forma, mesmo curvas elípticas ainda não são resistentes a um futuro ataque com Shor em computadores quânticos. Mas como eu expliquei no episódio anterior ainda estamos bem longe disso. E existem vários algoritmos novos na categoria de pós-quânticos que não dependem da dificuldade de fatorar primos.</p>
<p>Mesmo Curvas Elípticas continuam sendo suscetíveis a ataques de um possível futuro computador quântico também. Mas existe uma evolução disso que é o Supersingular Isogeny Graph. Parafraseando da Wikipedia, gráficos de isogenia supersingulares são classes de expansores de gráficos que aparecem em computação da teoria dos números e foram aplicados em criptografia de curva elíptica. Os vértices representam curvas elípticas supersingulares sobre campos finitos e as bordas representam isogenias entre curvas&hellip; Pronto, fodeu, eu não consigo nem ter um insight de como isso funciona. Mas, uma das propriedades importantes é que ele consegue ser mais seguro ainda com chaves ainda menores e é considerado pós-quântico. Quer dizer que é resistente aos algoritmos quânticos. Repetindo o que eu disse no episódio anterior, o mundo da criptografia não pretende ficar de braços cruzados esperando o computador quântico chegar. Povo tá antecipado. Em criptografia você sempre precisa estar um passo à frente.</p>
<p>E com isso fechamos o tema de conceitos básicos de criptografia. E eu não estou brincando, tudo que eu disse até aqui é o básico do básico de criptografia, especialmente se você tem interesse em criptanálise. Um dos campos da computação onde matemática é absoluto é criptografia. Mas mesmo sem saber a matemática toda, a intenção foi expor vocês a todos esses jargões e como eles se relacionam uns com os outros. Espero que agora você tenham uma noção melhor de onde cada jargão é usado, o que significam, quais os pontos fortes e fracos, e também porque você não deve sair tentando criar soluções que envolvem criptografia. Os ataques que eu falei aqui são todos “simples” pros especialistas e hackers. White hats e Black hats estão usando isso e coisas mais avançadas no arsenal deles. Novamente, eu tenho certeza que todo mundo ainda tem um monte de dúvidas, mas não deixem de discutir nos comentários abaixo. Se curtiram o vídeo mandem um joinha, compartilhem com seus amigos, assinem o canal e não deixem de clicar no sininho pra não perder os próximos episódios. A gente se vê, até mais.</p>
]]></content:encoded><category>seguranca</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #67 - Entendendo Conceitos Básicos de CRIPTOGRAFIA | Parte 1/2</title><link>https://akitaonrails.github.io/2019/11/21/akitando-67-entendendo-conceitos-basicos-de-criptografia-parte-1-2/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/11/21/akitando-67-entendendo-conceitos-basicos-de-criptografia-parte-1-2/</guid><pubDate>Thu, 21 Nov 2019 11:00:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/CcU5Kc_FN_4"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;h2&gt;DESCRIPTION&lt;span class="hx:absolute hx:-mt-20" id="description"&gt;&lt;/span&gt;
&lt;a href="#description" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Errata:&lt;/p&gt;
&lt;p&gt;Quando eu explico HMAC, claro que é uma simplificação mas eu mostro um exemplo de função chamada &amp;ldquo;hmac_sha1&amp;rdquo; e alguém pode achar que isso é a implementação completa, MAS NÃO É. Portanto NÃO copie do jeito que eu fiz, use a função de HMAC de uma biblioteca decente como a libsodium.&lt;/p&gt;
&lt;p&gt;Esta semana é a parte 1 desta micro-série de 2 episódios sobre Criptografia.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/CcU5Kc_FN_4"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<h2>DESCRIPTION<span class="hx:absolute hx:-mt-20" id="description"></span>
    <a href="#description" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Errata:</p>
<p>Quando eu explico HMAC, claro que é uma simplificação mas eu mostro um exemplo de função chamada &ldquo;hmac_sha1&rdquo; e alguém pode achar que isso é a implementação completa, MAS NÃO É. Portanto NÃO copie do jeito que eu fiz, use a função de HMAC de uma biblioteca decente como a libsodium.</p>
<p>Esta semana é a parte 1 desta micro-série de 2 episódios sobre Criptografia.</p>
<p>No último episódio falei sobre o impacto da Supremacia Quântica sobre a criptografia atual, mas muita gente não tem muita noção do que realmente significa criptografia.</p>
<p>Então no episódio de hoje vamos começar com alguns conceitos muito básicos sobre encriptação simétrica, alguns algoritmos famosos como DES e AES, entender como acontecem alguns tipos comuns de ataques como dicionário ou extensão de comprimento, e entender o que são de verdades os hashes que programadores usam todos os dias sem saber.</p>
<p>Pule direto pras seções:</p>
<p>07:41 - guardar senha no banco
09:32 - ciphers de substituição
11:11 - Enigma
14:57 - DES
19:43 - AES
22:48 - Hashes
26:53 - Colisões
30:12 - Extensão de Comprimento
35:36 - Dicionários
37:35 - Aniversários</p>
<p>Links:</p>
<ul>
<li>The Enigma Machine Online (<a href="https://cryptii.com/pipes/enigma-machine"target="_blank" rel="noopener">https://cryptii.com/pipes/enigma-machine</a>)</li>
<li>Can I break an uncracked Enigma code message? (<a href="https://www.theguardian.com/technology/2006/mar/02/guardianweeklytechnologysection2"target="_blank" rel="noopener">https://www.theguardian.com/technology/2006/mar/02/guardianweeklytechnologysection2</a>)</li>
<li>Cracking the Uncrackable: How Did Alan Turing and His Team Crack The Enigma Code? (<a href="https://www.scienceabc.com/innovation/cracking-the-uncrackable-how-did-alan-turing-and-his-team-crack-the-enigma-code.html"target="_blank" rel="noopener">https://www.scienceabc.com/innovation/cracking-the-uncrackable-how-did-alan-turing-and-his-team-crack-the-enigma-code.html</a>)</li>
<li>Everything you need to know about hash length extension attacks (<a href="https://blog.skullsecurity.org/2012/everything-you-need-to-know-about-hash-length-extension-attacks"target="_blank" rel="noopener">https://blog.skullsecurity.org/2012/everything-you-need-to-know-about-hash-length-extension-attacks</a>)</li>
<li>Hash Length Extension Attacks (<a href="https://www.whitehatsec.com/blog/hash-length-extension-attacks/"target="_blank" rel="noopener">https://www.whitehatsec.com/blog/hash-length-extension-attacks/</a>)</li>
<li>I have been Pwned? (<a href="https://haveibeenpwned.com"target="_blank" rel="noopener">https://haveibeenpwned.com</a>)</li>
<li>Understanding the Birthday Paradox (<a href="https://betterexplained.com/articles/understanding-the-birthday-paradox/"target="_blank" rel="noopener">https://betterexplained.com/articles/understanding-the-birthday-paradox/</a>)</li>
<li>Shattered: We have broken SHA-1 in practice. (<a href="https://shattered.io"target="_blank" rel="noopener">https://shattered.io</a>)</li>
<li>Divide and Conquer: Cracking MS-CHAPv2 with a 100% success rate (<a href="https://web.archive.org/web/20160316174007/https://www.cloudcracker.com/blog/2012/07/29/cracking-ms-chap-v2/"target="_blank" rel="noopener">https://web.archive.org/web/20160316174007/https://www.cloudcracker.com/blog/2012/07/29/cracking-ms-chap-v2/</a>)</li>
<li>Secure your data with AES-256 encryption (<a href="https://www.atpinc.com/blog/what-is-aes-256-encryption"target="_blank" rel="noopener">https://www.atpinc.com/blog/what-is-aes-256-encryption</a>)</li>
<li>Rainbow Tables: Your Password&rsquo;s Worst Nightmare (<a href="https://www.lifewire.com/rainbow-tables-your-passwords-worst-nightmare-2487288"target="_blank" rel="noopener">https://www.lifewire.com/rainbow-tables-your-passwords-worst-nightmare-2487288</a>)</li>
<li>Hash Length Extension Attacks(<a href="https://www.whitehatsec.com/blog/hash-length-extension-attacks/"target="_blank" rel="noopener">https://www.whitehatsec.com/blog/hash-length-extension-attacks/</a>)</li>
</ul>
<h2>SCRIPT<span class="hx:absolute hx:-mt-20" id="script"></span>
    <a href="#script" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olá pessoal, Fabio Akita</p>
<p>No último episódio eu tentei responder a pergunta se computação quântica tem o potencial de acabar com os esquemas de segurança que usamos hoje. Mencionei coisas como RSA ou AES. Eu imagino que não só iniciantes, mas mesmo desenvolvedores de software que já tem alguma experiência tem dificuldades de entender mesmo o básico sobre criptografia.</p>
<p>Cybersecurity é um tema gigante. Existem as tecnologias, as ferramentas e os processos. Segurança da informação ou Infosec é uma pequena parte de cybersecurity e foca mais na implementação e manutenção de processos e políticas, é onde você vê a maioria das certificações. Muito de segurança é uma questão de procedimentos.</p>
<p>Porém, essa parte me interessa muito pouco, pelo menos por agora. A idéia hoje é começar a explicar alguns dos jargões e conceitos pra que você se confunda menos quando estiver lidando com aspectos de segurança, em particular com criptografia. Vou repetir de novo porque é difícil de explicar a dimensão desse, é uma área gigante, com uma história longa, muita matemática, e você pode dedicar sua vida inteira estudando só isso e ainda não saber tudo. É tão grande na real que até este script, que é só um mero resuminho, ficou gigante e por isso tive que dividir em duas partes. Hoje vai ser a parte 1, semana que vem termino na parte 2. Peguem papel e caneta e se preparem, porque vocês vão precisar anotar bastante coisa pra estudar em mais detalhes depois.</p>
<p>E falando em segurança, agora é uma boa hora pra agradecer o patrocinador deste vídeo. Se você quer evitar que suas senhas sejam vazadas, ou evitar que o FBI descubra os sites de pornografia e bittorrent que você usa, devia estar usando uma VPN. E porque não usar NordVPN &hellip;. não não não, tô brincando, este episódio não é patrocinado pela NordVPN ou qualquer outra empresa, rola a vinheta!!</p>
<p>Se eu fosse chutar, eu diria que a maioria esmagadora dos programadores não pensa em segurança nem o mínimo que deviam. Sim, você mesmo aí assistindo. E eu também de vez em quando. Isso era assim 20 anos atrás e continua sendo assim hoje. O que mudou é que nos últimos 15 anos pelo menos criamos frameworks e práticas que todo mundo usa hoje meio sem pensar, e por causa disso muitos dos erros triviais foram evitados e você nem sabe disso. Por exemplo, um erro trivial e muito comum antigamente era codificar manualmente comandos SQL pra banco de dados numa string, concatenar o que o usuário mandava de um formulário, tipo de login, por exemplo, e mandar esse comando pro banco de dados. Como este exemplo aqui do lado. Mas se o usuário fosse minimamente esperto, nem precisa ser muito inteligente, sabendo que é assim que o programador faz, ele manda exatamente a string que se concatenada envia outros comandos arbitrários de SQL que o programador não estava esperando. Assim como nesse exemplo aqui do lado. Pronto, você tem um problema de SQL Injection na sua aplicação.</p>
<p>Praticamente todo framework web como Rails, Django, Laravel, ASP.NET e todos os outros hoje vem como algum tipo de ORM que é uma biblioteca de mapeamento objeto pra relacional, não só pra mapear tabelas com classes mas também fazer coisas como sanitização dos valores enviados por usuários. Daí a maioria de casos como SQL Injection são eliminados automaticamente. Além de SQL Injection a maioria dos frameworks competentes protege contra coisas como CSRF que é Cross Site Request Forgery, Session Hijacking, Replay Attacks e muito mais. Se não fizer no mínimo essas coisas, não é um bom framework.</p>
<p>Mas eu como disse antes. A maioria dos programadores não pensa em segurança. Daí você lê em algum post de blog, por exemplo, pra em vez de deixar o ORM gerar o comando SQL, se você escrever o comando manualmente e não usar nenhum ORM, pode ser que tenha mais performance. Pronto, você acabou de deixar de usar os mecanismos de segurança do framework pela promessa de performance. Isso é o mais comum. Programador é um bicho meio burro, e não devia ser. Nós devíamos ser de Ciência da Computação, cadê a ciência em ficar ouvindo promessas e implementando sem saber se funciona ou não? Tem hora que programador parece que tá escolhendo dieta da moda. Na dúvida é melhor assumir primeiro que se o framework guia você pra desenvolver de uma certa maneira deve ter razões pra isso. Especialmente se é usado por milhares de pessoas faz muitos anos. Entenda quais são essas razões antes de decidir não usar. Não estou falando de frameworkzinho hipster da moda que acabou de ser lançado. Bons frameworks são como bons vinhos, precisam de anos pra amadurecer. Só que aí programador iniciante tem mania de achar que sabe mais que as milhares de pessoas que gastaram milhares de horas homem amadurecendo o framework e decide: ah, vou fazer meu próprio framework porque eu sei fazer melhor. Pronto, você acabou de ganhar uma catástrofe esperando pra acontecer.</p>
<p>O trabalho de um programador é conseguir desenvolver alguma coisa o mais rápido possível, com a melhor qualidade possível. E qualidade significa não só funcionar como foi pedido como funcionar com a melhor performance, com a melhor escalabilidade e com a melhor segurança, e ainda tendo a melhor mantenabilidade. Só que é impossível ter 100% de todas essas características. Não existe almoço de graça. O trabalho de um programador é encontrar o meio do caminho entre todos esses parâmetros.</p>
<p>Esse balanço de tradeoff, ou de custo-benefício, não é simples. Aqui entra a experiência de ter trabalhado com outros programadores mais experientes e de ter visto atrasos, dificuldades de manutenção, buracos de segurança e gargalos de performance, na prática, em diversos projetos diferentes. Só porque alguma coisa &ldquo;roda&rdquo; não significa que ela funciona corretamente. A maioria dos cursos e tutoriais se preocupa em fazer alguma coisa &ldquo;rodar&rdquo;, mas não se preocupa com todos esses outros aspectos: segurança, performance, escalabilidade, produtividade e mantenabilidade.</p>
<p>Por exemplo, nos anos 90 quando todo mundo ainda tava aprendendo como programar pra web, não havia frameworks decentes. Lógico, a Web comercial tinha acabado de nascer. Não tinha boas práticas bem definidas como temos hoje. Então digamos que você quisesse fazer um sitezinho com login, página de produtos, um checkout pra colocar cartão de crédito. Como se fazia? Simples: um formulário HTML que dava POST pra um script Perl ou PHP, que recebia a senha e o cartão e gravava direto numa tabela no banco. Ah sim, o banco e o site eram instalados na mesma máquina porque esses recursos custavam caro. E como quem programa site normalmente tinha e ainda tem pouco conhecimento de coisas como configuração de infra, então instalava tudo sem senha ou colocava a senha hardcoded no código. Desastre, lógico.</p>
<p>Na verdade eu não falei certo, isso não é só antigamente, todo iniciante começa fazendo exatamente desse mesmo jeito, não importa a linguagem. A diferença é que nos anos 90 todos mundo era iniciante ainda, mas com o tempo fomos evoluindo. Entenda isso: como iniciante você não tem como saber até onde dá pra ir, por isso você é iniciante. Mas não se preocupe, parta do princípio que só porque Você não sabe, não quer dizer que não existe. E só porque roda não quer dizer que você fez certo. O problema é um iniciante colocar um site em produção sozinho. Até hoje, quando eu clico em recuperar a senha, tem site que manda a minha senha, aberta, no meu e-mail. Em 2019, isso é ridículo. Uma coisa é você estar treinando e fazendo errado. Isso não tem problema, faz parte do aprendizado. Outra coisa é achar que só porque fez meia dúzia de tutoriais já sabe tudo e colocar seus usuários em risco. Isso é irresponsabilidade.</p>
<p>Como desculpa para iniciar alguns conceitos, vamos pegar o exemplo de guardar as senhas em plaintext, ou seja, em texto aberto, numa tabela do banco de dados, como eu disse que um amador faria se fizesse tudo do zero, sem usar nenhum framework. Vamos assumir que existe uma possibilidade real dessa tabela ser roubada por um hacker. Isso é muito mais comum do que você pensa, empresas gigantes de LinkedIn até marcas famosas como Sephora, sites de relacionamentos como Adult Friend Finder, games como Clash of Kings e muito mais que você pode procurar no site I have been pwned já tiveram dados dos usuários expostos. Nesse site aliás, você pode digitar seu e-mail e ver se sua senha já foi exposta. Toda tabela de senhas uma hora vai ser roubada, não é uma questão de “se”, é só uma questão de quando. Em segurança, nunca assuma que existe segurança, sempre parta do pressuposto do pior caso, que os dados vão ser expostos, mais cedo ou mais tarde. Seu trabalho é garantir que seja o mais tarde possível.</p>
<p>A segunda coisa que alguém poderia pensar pode ser, “ok, não tem problema, e se eu encriptar a senha antes de gravar no banco?” Primeiro vamos entender o que significa encriptação. Em criptografia normalmente estamos nos referindo à uma encriptação simétrica. De maneira bem simples, pense uma função onde passamos algum texto e um segredo como entrada e a saída é o texto bagunçado. Simétrico porque se passarmos o texto bagunçado de volta, com o mesmo segredo, revertemos de volta pro texto original. Isso é decriptação. As primeiras versões de encriptação era a coisa mais simples que você poderia imaginar: trocar uma letra por outra ou simplesmente dar shift e mover as letras pra frente ou pra trás como nos algoritmos Caesar ou ROT13. Esses normalmente são usados em cursos introdutórios de criptografia, tipo um hello world, por serem super triviais.</p>
<p>Um Caesar é basicamente um cipher de substituição. Primeiro escolhemos um valor de shift, por exemplo 2 pra ficar simples, esse é o segredo. Então toda letra A, dois pra frente, vira um C, toda letra B vira um D, toda letra C vira um F, sempre dois pra frente e assim por diante. A chave secreta é esse valor de shift 2. Uma forma de quebrar isso? Se você notar que se trata de algo parecido com um algoritmo Caesar, você pode tentar via força bruta que é tentar todos os 25 possíveis valores de shift já que o alfabeto só tem 26 letras, você pode facilmente tentar um a um até descobrir, no papel, em poucos minutos.</p>
<p>Outra forma é usar análise de frequência, por exemplo, se eu sei que a mensagem original é em inglês, as letras que aparecem com mais frequência na maioria das palavras são letras “E” ou “A”. Digamos que na mensagem encriptada a letra que mais aparece é “P”, e eu podia chutar que originalmente é um “E” e na verdade o shift foi de 11 letras. Se não funcionar, eu poderia chutar que é um “A” e que o shift foi de 15 letras. Daí em vez de tentar todas as 25 possibilidades na força bruta, só duas ou três tentativas já seria suficiente. Veja que mesmo nesse exemplo simples, podemos diminuir a dificuldade da força bruta de 25 tentativas pra dois ou três, quase 10 vezes mais rápido. Qualquer tentativa de quebrar uma encriptação, na pior das hipóteses é via força bruta, mas com criptanálise, como esse básico que mencionei aqui, ou seja analisando a mensagem encriptada, que chamamos de ciphertext, podemos usar matemática, mais precisamente estatística, pra diminuir as tentativas algumas ordens de grandeza.</p>
<p>O ROT13 é mais uma brincadeira, ele é um caso especial do Caesar com shift fixo de 13 letras. Aliás, o Caesar original foi usado pelos Romanos, por isso se chama Caesar e assim como o ROT13 também tinha um valor de shift fixo, acho que 3 ou 5. De qualquer forma, uma coisa que tem que ficar muito claro: o dado encriptado não pode compartilhar características com a mensagem original, porque via criptanálise eu conseguiria inferir alguma informação. Outro caso clássico ensinado como introdução em criptanálise é o famoso cipher Enigma usado pelos alemães na Segunda Guerra, que também era um cipher de substituição. Recomendo que dêem uma procurada em canais como Numberphile que explica ele em detalhes, mas o equipamento era tipo uma máquina de escrever onde cada letra que você teclava devolvia uma outra letra, qualquer uma das outras 25 letras do alfabeto, menos ele mesmo. E ele não substitui todo “A” pela mesma letra toda vez, cada A no texto vai virar uma letra diferente. A máquina é configurada conectando pares de letras no que se chama de plugboard, e configurando três rotores e um refletor que fazia o embaralhamento. Sem entrar em detalhes, se você souber a configuração desse plugboard, dos rotores e do refletor, daí você conseguia reverter a mensagem encriptada.</p>
<p>O grande erro dos alemães e o grande insight de Alan Turing foi notar que todo dia de manhã os alemães mandavam um relatório do tempo. E todo relatório começava com as mesmas palavras no topo da mensagem, tipo “tempo” que seria “Wetterbericht” em alemão e terminava com “Hail Hitler”. Daí ele entendeu que apesar das letras serem embaralhadas, a mesma letra nunca virava ela mesma. Então você conseguia chutar os pares de letras no plugboard e ir tentando decifrar letra a letra, mas uma hora dava errado porque o próximo par colidia com um que você já tinha achado que sabia. Então precisava começar de novo, mas como as letras nunca voltavam pra elas mesmas, a combinatória inteira de todos os pares que você testou podia ser descartada e só testar outra combinação inicial diferente dos pares que você já tentou.</p>
<p>E era isso que fazia a máquina que o Turing construiu, o tal Bombe. Se você já viu uma foto parecendo uma estante com vários rotores, a cada rotação ele vai tentando um espaço de combinações, não funcionando, descarta o espaço inteiro e tenta de novo, o que sobrar é a resposta certa. E ele conseguia achar a configuração em uns 20 minutos, de novo, dado esse insight de que a mesma letra nunca vira ela mesma e sabendo uma palavra que aparece em toda mensagem. Na força bruta, sem esses insights, só testando cada configuração possível do Enigma, um computador desktop dos dias de hoje ainda levaria de horas a dias pra conseguir decifrar. De novo, o que Turing e sua equipe conseguiram fazer, foi usar criptanálise pra diminuir drasticamente o espaço de tentativas.</p>
<p>Essa história toda foi pra explicar duas coisas. Uma função de encriptação simétrica com um segredo, no caso a combinação dos rotores, é construída de tal forma que via força bruta seria computacionalmente inviável. No caso do Enigma existem mais de milhões de milhões de milhões de possibilidades. Mas como ele tinha os defeitos que eu falei, mesmo nos anos 40, sem um computador potente como temos hoje, já foi possível derrotar o algoritmo em 20 minutos com rotores mecânicos. A segunda coisa: criar um algoritmo de encriptação robusto, resistente a criptanálise, é muito mais difícil do que parece. Se você não é um bom matemático teórico, nunca, jamais tente inventar seu próprio algoritmo. Sua melhor tentativa vai ser pior do que o Enigma.</p>
<p>Nos UNIX originais nos anos 70 o crypt foi implementado como uma variação de uma máquina de rotores, parecido com o algoritmo do Enigma. Algumas versões eram ainda mais fracas, e literalmente implementavam o Caesar que eu falei acima. O ROT13 não era brincadeira, ele chegou a ser usado de verdade. Essas implementações foram feitas numa época onde não havia a preocupação de hoje em criptografia. Feitos por gente como eu ou você, sem treinamento em matemática, era só pra deixar as coisas um pouco mais difíceis. Em breve o Caesar foi substituído no comando crypt por algoritmos melhores como o Data Encryption Standard ou DES.</p>
<p>O DES foi desenvolvido pela IBM e escolhido pelo NIST ou National Institute of Standards and Technology como o padrão de encriptação do governo americano nos anos 70, usando um segredo, ou chave, de 56-bits. Relembrando, significa um espaço de dois elevado a 56 ou 72 quadrilhões de possíveis chaves. Como vocês viram na história do Enigma e de algoritmos triviais de substituição, uma das fraquezas mais óbvias é encontrar um padrão ou relação estatística entre a mensagem original em plaintext e o ciphertext. Dentre as várias coisa que o DES implementou foram S-boxes ou substitution boxes. A partir daqui começa a entrar matemática mais complicada, coisas como funções de Bent, transformação Afim, transformada de Walsh e por aí vai. Mas basta entender que S-boxes são tabelas de substituição que causam o que Claude Shannon chama de confusão, tentando eliminar qualquer correlação ou relacionamento entre o plaintext da entrada e o ciphertext da saída.</p>
<p>Pra evitar a situação do “Hail Hitler” do Enigma onde conseguiríamos inferir alguma coisa do plaintext a partir do ciphertext, criou-se a idéia de quebrar a mensagem original em blocos. Digamos de 128 bits, passar por várias rodadas de transformação usando coisas como os S-boxes e usar o ciphertext desse bloco como entropia pro bloco seguinte. De tal forma que mesmo que o texto tenha trechos que se repetem, eles não vão ser encriptados da mesma forma, e assim no ciphertext final não temos como encontrar padrões. Mesmo assim, o mesmo plaintext aplicado no mesmo algoritmo sempre vai resultar no mesmo ciphertext e pra evitar isso também costuma se usar um Initialization Vector ou IV no primeiro bloco. Dessa forma, o plaintext com a chave secreta, variando esse IV, o ciphertext vai ser bastante diferente.</p>
<p>Além do princípio da confusão de Shannon ainda existe o princípio da difusão, ou seja, se mudarmos 1 bit no plaintext, pelo menos metade dos bits do ciphertext deveriam mudar de lugar. Diminuindo ainda mais a correlação do ciphertext com o plaintext. Enfim, não precisam decorar o que eu acabei de falar. A idéia é mais explicar como as coisas evoluíram desde o famoso Enigma.</p>
<p>De qualquer forma, o DES de 56-bits já foi quebrado. Uma das formas hoje de fazer isso é via FPGA ou Field Programmable Gate Arrays. Pense num chip genérico onde podemos desenhar circuitos e reprogramar esse chip pro tipo de circuito que quisermos. Se vocês assistiram o começo do meu vídeo sobre Supremacia Quântica eu rapidamente explico sobre portas lógicas e como podemos criar circuitos com essas portas. FPGAs é isso: reprogramação das portas lógicas, pra criar um hardware especializado. Enfim, hoje existem FPGAs que conseguem gerar da ordem de terachaves por segundo, no caso quase 800 bilhões de chaves por segundo e como eu falei que 56-bits dá um total de 72 quadrilhões de chaves, significa que dá pra quebrar o DES hoje em pouco mais de 26 horas, via força bruta. FPGAs não foram criados só pra isso claro, eles existiam antes, mas criptografia sempre vai usar o que tem de melhor pra estar um passo à frente.</p>
<p>O DES foi muito importante porque ele recebeu um escrutínio enorme. E como o governo americano estava usando, havia muita motivação pra entender como ele funciona, e obviamente como tentar quebrar. Lembrando, ele foi inventado nos anos 70. Esse sistema com FPGAs que eu falei foi criado em 2012. E mesmo assim leva 26 horas pra quebrar, não é instantâneo. Quando as primeiras teorias de como quebrar o DES começaram a aparecer, também surgiram formas de deixar o próprio DES mais forte e uma dessas formas é literalmente rodar o DES mais de uma vez. Mais especificamente, passar cada bloco da mensagem pelo DES três vezes, e isso é o que conhecemos hoje como TripleDES. Teoricamente ele é computacionalmente inviável de quebrar mas pouca gente usa TripleDES hoje. Isso porque o DES não é particularmente rápido e TripleDES, como você pode imaginar pode ser três vezes ainda mais lento.</p>
<p>Então o NIST comissionou um novo algoritmo e depois de um longo processo e diversos bons novos candidatos, eles escolheram o algoritmo Rijndael cujo nome é derivado dos nomes dos autores, Joan Daemen and Vincent Rijmen. Diferente do DES que usa chaves de 56-bits, o Rijndael usa chaves de 128, 192 ou 256 bits.
Agora que vocês sabem que o sistema de FPGA mais rápido de 2012 demorava 26 horas pra percorrer o espaço de 56-bits, lembram que eu expliquei no episódio anterior que cada mais um bit que você adiciona duplica o espaço? Uma chave de 256-bits é 2 elevado a 256 que dá um número tão astronômico que tamos falando de um número com 77 casas. O quadrilhão que eu falei acima tem só umas 15 casas. O algoritmo usa essa chave pra derivar outras 10 chaves, usa os tais S-boxes que eu falei, passa cada bloco por 10 a 13 rodadas de transformações mais complicadas que o DES. E com tudo isso ele ainda é três vezes mais rápido que o DES.</p>
<p>O algoritmo Rijndael foi batizado pelo NIST como Advanced Encription Standard ou AES. Mas as técnicas de criptanálise evoluíram também. Hoje em dia sem saber que algoritmo ou que chave é usada, existem análises que checam quanto tempo tá levando cada trecho do algoritmo, o consumo de energia em cada etapa, a radiação eletromagnética e muito mais pra conseguir mais dados pra diminuir as possibilidades de tentativas, já que força bruta pura num espaço de 256 bits é computacionalmente inviável. E uma das formas de mitigar isso que se chama de ataques de side-channel é que hoje em dia o processador implementa o AES direto em hardware, pra evitar que implementações de software possam ser analisadas quando rodam.</p>
<p>Já faz 10 gerações atrás que a Intel implementa essas instruções no processador, desde uma antes da Sandy-Bridge. A AMD também implementa desde o Jaguar. E é o AES-256 que você provavelmente usa se tem partição encriptada. Com tudo isso, ela é extremamente rápida, o suficiente pra encriptar e decriptar dados do seu HD em tempo real, e é absurdamente segura. Eu concluí no episódio de Supremacia Quântica, que nem um computador quântico seria capaz de quebrar o AES, ele só diminuiria a força bruta em uma ordem de grandeza, mas o espaço computacional é tão gigante que ele continua inviável de quebrar mesmo assim.</p>
<p>Eu queria dizer que encriptação simétrica de ciphers de bloco são funções que recebem um plaintext, um segredo e um initialization vector e cospe um ciphertext seguro. Mas isso não explica o que realmente significa ser seguro, então resolvi fazer essa tangente. Acho que vale a pena ter uma noção do que significa algo ser encriptado com AES. Mas eu não demonstrei como funciona essa geração de chaves a partir da chave de 256 bits, como ele gera as tabelas, os S-boxes, as várias rotações e como isso aumenta a confusão e difusão segundo Shannon. Se tiver interesse existem centenas de documentações e videos aqui no YouTube mesmo explicando o processo.</p>
<p>Agora, existe uma outra categoria de algoritmos que é comumente confundido com encriptação e são as funções criptográficas de hash ou message digest, literalmente funções que digerem mensagens. Diferente de encriptação simétrica como o TripleDES ou AES, onde você pode reverter o processo e decriptar do ciphertext pro plaintext, algoritmos de hashing são o que chamamos de one-way, direção única, ou seja, irreversíveis. Toda função de hashing pega um plaintext de qualquer tamanho e cospe uma saída de tamanho fixo. Por exemplo, o antigo MD5 sempre vai cuspir uma saída de 128 bits, representado como uma string de 32 caracteres em hexadecimal. Outro que vocês devem conhecer é o SHA1, ou Secure Hash Algorithm 1, que cospe digests de 160 bits representados em uma string de 40 caracteres em hexadecimal.</p>
<p>Nos primórdios da internet, com conexões instáveis e pouco confiáveis, pra garantir que o download de um arquivo não veio corrompido era usado o antecessor do MD5, o MD4 pra comparar os hashes original e o do arquivo downloado. Um MD5 é basicamente o MD4 com mais uma rodada. Já já vou explicar o que é uma rodada.</p>
<p>Ambos MD5 e SHA1 tem seus usos, mas são considerados inseguros hoje em dia. No lugar deles deve ser usado SHA2 ou o SHA3 que foi aprovado pelo NIST em 2015, a partir do algoritmo chamado Keccak. Vocês devem ter notado que o NIST tem uma nomenclatura meio padrão, AES pra encriptação é o Rijndael, SHA3 é o Keccak, isso pode confundir. Mas fora dos meios de criptografia, poucos ouvem falar em Rijndael ou Keccak e você ouve mais AES ou SHA. Mas enfim, como funciona e pra que servem algoritmos de hashing?</p>
<p>Eu disse que a função recebe uma entrada de qualquer tamanho, seja uma string de senha, seja um arquivo PDF de contrato, seja um arquivo binário do instalador de algum programa, e sempre vai cuspir uma saída de tamanho fixo, de 128 bits no caso do MD5 ou 160 bits no caso do SHA1. Pra fazer isso um MD5, por exemplo, você divide o binário do arquivo e vai processando em blocos de 128-bits. É um cipher de bloco, só que em vez de preservar os blocos numa cadeia, aqui ele usa o processamento do bloco anterior em cima do próximo bloco, sobrando sempre um bloco de tamanho fixo. Em cima de cada bloco é aplicado alguma computação, e é aí que os algoritmos diferem pra ganhar as características desejadas de segurança. Pra simplificar, imaginem que o texto original é este blob em binário aqui do lado, de 3 linhas só.</p>
<p>Agora vamos quebrar esse blob em blocos de 10 bits em vez de 128, de novo, pra simplificar. De forma extremamente ingênua, vamos só aplicar um exclusive OR ou XOR que eu expliquei no vídeo anterior, um bloco de cada vez. Um XOR da primeira linha com a segunda linha. E agora o resultado com a terceira linha, mas temos um problema, a terceira linha não tem 10-bits, pra poder executar o XOR precisamos completar ela, e pra isso fazemos o que se chama de padding. Se não estou enganado um MD5 continua o que falta colocando primeiro um bit 1 e o resto com zeros. Pronto, agora fazemos o último XOR. E no final, temos um único bloco de 10-bits e isso seria uma função de hashing de 10-bits que é extremamente inseguro porque eu só usei um mísero XOR. Claro que o MD5 faz mais coisas por bloco, mas só pra vocês terem uma noção de como a partir de uma entrada de tamanho arbitrário, chegamos numa saída de tamanho fixo.</p>
<p>Novamente, não implementem uma função de hashing assim como eu mostrei. Um MD5 usa tabelas pré-computadas, divide cada bloco de 128 bits em quatro sub-blocos de 32-bits, performa várias operações além de XOR, como AND, OR, e mais, e depois de vários rounds de operações concatena os sub-blocos de volta nos 128-bits e repete pra cada bloco. É um pouco mais complicado e mesmo assim o MD5 e o SHA1 não são mais considerados seguros. Eles tem pelo menos dois problemas, sofrem de ataque de colisão e ataque de extensão de comprimento.</p>
<p>Pra entender colisão precisamos entender algumas das características desejáveis em funções de hashing. Primeiro, gostaríamos que o hash que ela cospe, que é um número binário, seja difícil de distinguir de uma função que cospe números aleatórios. Ou seja, não seria bom que a saída se concentrasse próximo de certos números e sim que ela tivesse uma distribuição normal. Como a frequência de números jogando dados. Nunca se concentrando só num número. Vamos dar um exemplo de uso. Digamos que eu tivesse a tarefa de organizar centenas de livros em dezenas de estantes numa biblioteca. Como organizar?</p>
<p>A primeira coisa que vem à cabeça seria talvez organizar em ordem alfabética por título ou autor. Porém, rapidamente você vai notar que tem mais livros que começam com certas letras do que outras, por exemplo, que começam com a letra A do que com Z, faz de conta. Mesmo se eu organizasse assim, eu ainda ia ter que ficar caçando em qual estante começa e em qual prateleira começa qual letra. Em vez disso poderíamos numerar cada posição em cada estante, digamos que cada estante comporte 200 livros e temos 20 estantes então temos espaço pra 4000 livros. Temos 1000 livros pra organizar, e poderíamos passar o título numa função de hashing e ele nos devolve um número entre 1 e 4000. É quase 12-bits, portanto seria uma função de hashing de 12-bits.</p>
<p>Agora, o que seria o ideal? Que cada título devolvesse um número não repetido, porque não seria legal se muitos livros caíssem na mesma posição, certo? Se caíssem elas entrariam em colisão. Não dá pra colocar dois livros no mesmo lugar físico nesse nosso exemplo. Essa é uma das coisas que é desejável numa função de hashing: que existam poucas ou o mais próximo de zero colisões. Mas justamente uma das fraquezas do MD5 e do SHA1 é que descobriram que se você pegar o título do livro e cuidadosamente alterar o título colocando digamos alguns zeros no final do título, uma hora chegaríamos no mesmo hash de outro título, criando uma colisão.</p>
<p>Por que isso é ruim no mundo real? Uma das utilidades de uma função de hashing é ser uma impressão digital. Digamos que eu tenha um contrato de 100 páginas em Word. Como eu sei que depois que eu transmiti o arquivo pra você via e-mail, ninguém interceptou o e-mail, alterou o contrato e mandou uma cópia adulterada? Simples, antes de enviar o contrato eu passo ela num MD5 que vai me cuspir uma impressão digital de 32 caracteres. Eu passo o arquivo pra você e te digo qual é o hash. Daí você pode passar o contrato pelo MD5 do seu lado e comparar com o meu hash e se bater sabemos que é o mesmo arquivo.</p>
<p>Porém, se a função é fraca contra ataques de colisão, alguém no meio do caminho poderia adulterar o contrato pra te foder, mudar as palavras cuidadosamente pra tornar o contrato ruim pra você. Só que isso iria alterar o hash. Mas aí quem interceptou, sabendo da fraqueza do MD5, faz pequenas alterações como adicionar ou tirar uma vírgula, um ponto, alguns espaços e é possível chegar no mesmo hash do arquivo original. Aí você recebe o arquivo que foi cuidadosamente adulterado, roda ele no MD5 e ele te dá o mesmo hash do arquivo original. E você agora vai confiar que esse contrato é o válido e vai assinar sem ler. Isso é um ataque de colisão.</p>
<p>E o tal ataque de extensão de comprimento? Aqui vou simplificar bastante mas eu deixei o procedimento completo nos links na descrição abaixo. Digamos que você tenha feito um site pra fazer download de arquivos privados, um mini Dropbox. Você faz o upload de um arquivo e o site devolve uma URL com uma chave especial na URL. Muita gente faria assim, pegaria o nome do arquivo, concatenaria com algum segredo que fica no servidor e devolveria o hash feito com MD5 ou SHA1. Pra baixar de novo o arquivo, você precisa mandar a mesma URL com o nome do arquivo e esse hash. Em particular esse uso de hash pra autenticação é o que se chama de MAC ou Message Authentication Code. Toda vez que você usa um hash pra autenticação damos esse nome de MAC.</p>
<p>Enfim, digamos que você é um usuário malicioso querendo baixar outros arquivos do servidor diferente desse que deu upload. Digamos o arquivo /etc/passwd. Só que não temos o segredo do servidor pra concatenar com esse nome de arquivo e gerar um novo MAC, então como podemos fazer? Lembram como o hash é calculado no meu exemplo simples com XOR? O string é quebrado em blocos de 128-bits no caso do MD5 ou 160-bits no caso do SHA1, o último bloco provavelmente não vai ter o tamanho total então fazemos padding adicionando o bit 1 seguido de zeros até completar o tamanho do bloco. Daí fazemos várias operações em vários rounds em cima de cada bloco e usamos esse resultado em cima do bloco seguinte. O hash de cada bloco, no caso do SHA1, é a composição de 4 sub-blocos de 32-bits, que é concatenado pra gerar o hash intermediário até o último bloco que ele tinha. Daí vamos pro bloco seguinte, que tem nosso valor adulterado. Entendem? Tudo que a função de hash precisa pra processar o bloco seguinte é o hash dos blocos anteriores. Ou seja, sem saber o texto original passada pra função, se soubermos o hash, sabemos como processar novos blocos seguintes só adicionando o padding de zeros antes do nosso valor malicioso.</p>
<p>Pra gerar o tal MAC que é o hash com SHA1, o servidor concatena um certo segredo com o nome do arquivo, faz o padding até o tamanho do string ser divisível por 160 bits e vai aplicando o SHA1 um bloco em cima do outro. Sem saber o segredo, se eu souber aó o tamanho do segredo, eu posso fazer o padding de zeros manualmente a partir do nome do arquivo que eu fiz upload e iniciar um novo bloco com o nome do arquivo que eu realmente quero. E eu vou conseguir gerar um novo MAC mesmo sem saber o segredo, porque o hash que eu já tenho é tudo que a função precisa pra processar o próximo bloco. É meio complicado de visualizar mas imagina que sabendo só o hash que é esse MAC do arquivo original, eu posso configurar o estado da função do SHA1 pra continuar a partir desse ponto, como se ainda tivesse mais um bloco pra terminar de computar, que é o bloco malicioso que eu concatenei no final. O problema é que o hash gerado pelo MD5 ou SHA1 me dá todas as informações que preciso pra continuar a fazer ele rodar pra novos blocos e gerar hashes válidos.</p>
<p>Se não entenderam, revejam esse trecho, ou acreditem que só de saber o hash eu posso adicionar novos blocos e gerar um novo hash válido como se a mensagem original tivesse esse bloco adicional desde o começo. Mas tem um porém, justamente pra evitar essas coisas você concatena um segredo no começo da mensagem original, no caso o nome do arquivo. Mesmo que eu acredite que esse procedimento funcione, como eu vou saber o tamanho do segredo, que é importante pra saber quantos zeros eu preciso colocar de padding? Simples, digamos que a aplicação que eu estou tentando hackear é baseado num projeto open source, tipo um Wordpress ou Magento da vida. Basta ir no GitHub e ver que tamanho de segredo está sendo usado no código fonte e testar. De novo, eu não preciso saber o segredo, só o tamanho da string do segredo.</p>
<p>É por isso que existe HMAC, que talvez vocês já tenham esbarrado em algum tutorial. Pra ajudar a evitar ataques de colisão e ataques de extensão de comprimento como expliquei acima. Em vez de só aplicar o hash em cima segredo mais a mensagem. Depois de fazer esse hash, você aplica a função de hash de novo concatenando o segredo com o hash. Ou seja, rodamos a função de hash duas vezes uma em cima da outra concatenando o segredo nas duas vezes. Isso é um HMAC ou hash-based message authentication code. Você usa isso quando quer garantir que o conteúdo original da mensagem não foi adulterado. Algoritmos mais novos como SHA3 não precisa fazer esse procedimento, ele sozinho já é um HMAC.</p>
<p>Estão vendo como as coisas não são tão simples quanto vocês pensam? Todo mundo pensa que ah, basta eu concatenar um salt ou segredo numa string antes de aplicar o hash e ele vai já vai ser magicamente seguro. Por isso eu repito que vocês não devem subestimar segurança e ficar tentando fazer soluções de criptografia na mão. O que determina a força de um algoritmo não é só o tamanho da chave, é como você usa o algoritmo. E falando em subestimar, ataques de extensão de comprimento compromentem hashes usados como MACs ou seja, como métodos de verificação e autenticação, como no exemplo de download de arquivos. Mas e hashes usados pra obfuscar senhas, como no caso da tabela de senhas roubado?</p>
<p>Um procedimento padrão é receber a senha de um usuário no cadastro e nunca guardar a senha aberta e nem encriptar. Recapitulando, porque se a senha de encriptação for roubado, você consegue abrir 100% de todas as senhas da base. Você tem um ponto único de falha muito perigoso. Por isso a próxima idéia que muita gente teve foi fazer hash das senhas antes e gravar só esse hash no banco. Daí quando o usuário for fazer login, você faz o hash da senha que ele digitou e compara com o hash do banco, se bater, quase com certeza sabemos que é a senha certa. Parece promissor, não parece? Mas só guardar o hash da senha ainda é muito fraco.</p>
<p>A maioria dos usuários usa como senha palavras fáceis de lembrar. Incluindo coisas toscas como 12345678 ou teste123. Ou dados que estão abertos no seu cadastro, como data de aniversário ou algo parecido. Via força bruta, testando todo string possível, você levaria anos pra encontrar uma única senha. Mas os hackers não fazem força bruta. E se você pré-calculasse o hash de todas as palavras mais usadas de um dicionário? Agora você sai comparando o hash da tabela de senhas roubada com a tabela de hashes do dicionário. Numa base de dados grande como de uma rede social, com milhões de pessoas, você com certeza ia achar centenas ou milhares de senhas assim.</p>
<p>Ataques de dicionário ou ataques com rainbow tables usam tabelas pré-computadas de hashes. Lembrando que a possibilidade de colisões num algoritmo de hash é tecnicamente pequena mas não é zero. Duas senhas diferentes podem acabar colidindo no mesmo hash. Lembram do exemplo dos livros e estantes? Mesmo usando algoritmos que não tem as fraquezas de colisão do MD5 ou SHA1 como o SHA2, ainda assim sempre existe o risco do paradoxo do aniversário, já ouviram falar? Vale contar isso se você nunca ouviu antes porque novamente, o que chamamos de “senso comum” normalmente falha perto da estatística.</p>
<p>Imagine uma sala de aula, qual a chance de duas pessoas fazerem aniversário no mesmo dia? Você podia pensar que seria perto de zero, afinal uma sala de aula tem só 30 alunos, e existem 365 dias no ano. Porém, é mais frequente do que você pensa. Pra calcular é simples e vou deixar links nas descrições abaixo, mas pense assim. Em vez de tentar calcular a chance de duas pessoas terem o mesmo aniversário, é mais fácil calcular primeiro a probabilidade de ninguém na sala ter o mesmo aniversário. Então a primeira pessoa pode fazer aniversário qualquer dia do ano, então ele tem 365 dividido por 365, ou seja 1. A segunda pessoa só pode cair em 364 dos dias restantes, então 364 dividido por 365. A terceira pessoa só pode cair em 363 dos 365. E assim por diante. Multiplicamos todas as probabilidades, pros 30 alunos. Agora, fazemos 1 menos essa probabilidade pra ter a probabilidade de duas pessoas terem o mesmo aniversário, sabe qual o resultado? Numa sala de aula de 30 pessoas a probabilidade é de uns 70%! Tá bem longe de zero isso. Colisões são estatisticamente mais frequentes do que gostaríamos, é um fato da matemática. E é esse um dos perigos com algoritmos de hash, o fato que colisões são probabilisticamente possíveis.</p>
<p>Eu levanto esse fato porque muito programador usa hashes de algoritmos como SHA256 achando que eles sempre vão ser únicos e por isso poderiam ser usados até como identificadores únicos. Mas isso tá errado. Por acaso, a distribuição estatística de um message digest se comporta parecido com um gerador de números pseudo aleatórios, mas eventualmente haverá colisões como eu acabei de explicar. Por isso que existe outra coisa se você precisa de números únicos, que são os Universally Unique Identifiers ou UUID, ou a implementação da Microsoft que são os Globally Unique Identifiers ou GUID. Por acaso eles são também números de 128-bits, que é o mesmo comprimento da saída do MD5, representados como hexadecimais de 32 caracteres. Por isso a “cara” de um UUID é meio parecido com a saída de um MD5. UUID também tem chance de ter colisões, mas eles são desenhados pra essa probabilidade ser realmente quase zero. Quando acontecem, e já aconteceu algumas vezes, é considerado um bug na implementação.</p>
<p>Aliás, falando em strings parecidos, também não confundir com o novo padrão IPv6 que também é um endereço de 128-bits, e adivinhem, representados com 32 caracteres hexadecimais. Portanto, pra quem nunca viu nada disso, um endereço IPv6, um identificador único UUID ou GUID, e um hash de MD5, tem mais ou menos a mesma “cara”, sendo todos números de 128-bits, representados com 32 caracteres hexadecimais. Mas eles são gerados de formas diferentes e tem usos completamente diferentes.</p>
<p>E com isso eu vou terminar por hoje num cliffhanger! Exatamente neste ponto estamos na metade do assunto e o resto já está escrito. Mas agora é uma boa hora pra rever o que eu expliquei. Peguem suas anotações, façam mais pesquisa nos pontos que mais se interessaram e estudem bastante porque o próximo episódio vai ser tão denso quanto este. Não vou nem perguntar se vocês ficaram com dúvidas, porque até eu ainda tenho muitas dúvidas em muitas coisas. Mas não deixem de comentar abaixo, se curtiram o vídeo mandem um joinha, compartilhem com seus amigos, não deixem de assinar o canal e clicar no sininho pra não perder a parte 2. A gente se vê semana que vem, até mais.</p>
]]></content:encoded><category>seguranca</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #46 - Gerenciamento de Memória (Parte 2) | Entendendo Back-end para Iniciantes (Parte 6)</title><link>https://akitaonrails.github.io/2019/04/03/akitando-46-gerenciamento-de-memoria-parte-2-entendendo-back-end-para-iniciantes-parte-6/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/04/03/akitando-46-gerenciamento-de-memoria-parte-2-entendendo-back-end-para-iniciantes-parte-6/</guid><pubDate>Wed, 03 Apr 2019 17:01:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/DGU1awKrNiA"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Finalmente, chegamos no último episódio do tema de Back-End!&lt;/p&gt;
&lt;p&gt;Devo dizer que este foi um dos episódios que eu mais queria explicar. Toda nova linguagem hoje em dia tem Garbage Collector. Mas a maioria dos programadores não tem a mínima idéia de como eles funcionam.&lt;/p&gt;
&lt;p&gt;Mais importante: todos acreditam que garbage collectors são mágicos e &amp;ldquo;simplesmente funcionam&amp;rdquo; mas não entendem quais são os reais motivos de porque eles existem, quais problemas eles resolvem, e quanto eles CUSTAM pro seu programa. Sim! Nenhuma mágica vem de graça.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/DGU1awKrNiA"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Finalmente, chegamos no último episódio do tema de Back-End!</p>
<p>Devo dizer que este foi um dos episódios que eu mais queria explicar. Toda nova linguagem hoje em dia tem Garbage Collector. Mas a maioria dos programadores não tem a mínima idéia de como eles funcionam.</p>
<p>Mais importante: todos acreditam que garbage collectors são mágicos e &ldquo;simplesmente funcionam&rdquo; mas não entendem quais são os reais motivos de porque eles existem, quais problemas eles resolvem, e quanto eles CUSTAM pro seu programa. Sim! Nenhuma mágica vem de graça.</p>
<p>Hoje vamos usar o que aprendemos até agora pra finalmente olhar linguagens como Objective-C/Swift, Python, Ruby, Java, Erlang/Elixir, Go e entender como eles diferem no gerenciamento de memória.</p>
<p>Links:</p>
<ul>
<li>Sun Java System Application Server 9.1 Performance Tuning Guide (<a href="https://docs.oracle.com/cd/E19159-01/819-3681/abeio/index.html"target="_blank" rel="noopener">https://docs.oracle.com/cd/E19159-01/819-3681/abeio/index.html</a>)</li>
<li>Garbage collection in Python: things you need to know (<a href="https://rushter.com/blog/python-garbage-collector/"target="_blank" rel="noopener">https://rushter.com/blog/python-garbage-collector/</a>)</li>
<li>What causes Ruby memory bloat? (<a href="https://www.joyfulbikeshedding.com/blog/2019-03-14-what-causes-ruby-memory-bloat.html"target="_blank" rel="noopener">https://www.joyfulbikeshedding.com/blog/2019-03-14-what-causes-ruby-memory-bloat.html</a>)</li>
<li>Demystifying the Ruby GC (<a href="https://samsaffron.com/archive/2013/11/22/demystifying-the-ruby-gc"target="_blank" rel="noopener">https://samsaffron.com/archive/2013/11/22/demystifying-the-ruby-gc</a>)</li>
<li>Erlang Garbage Collector (<a href="https://www.erlang-solutions.com/blog/erlang-garbage-collector.html"target="_blank" rel="noopener">https://www.erlang-solutions.com/blog/erlang-garbage-collector.html</a>)</li>
<li>Go, don&rsquo;t collect my garbage (<a href="https://blog.cloudflare.com/go-dont-collect-my-garbage/"target="_blank" rel="noopener">https://blog.cloudflare.com/go-dont-collect-my-garbage/</a>)</li>
<li>Tuning glibc Memory Behavior (<a href="https://devcenter.heroku.com/articles/tuning-glibc-memory-behavior"target="_blank" rel="noopener">https://devcenter.heroku.com/articles/tuning-glibc-memory-behavior</a>)</li>
<li>The status of Ruby memory trimming &amp; how you can help with testing (<a href="https://www.joyfulbikeshedding.com/blog/2019-03-29-the-status-of-ruby-memory-trimming-and-how-you-can-help-with-testing.html"target="_blank" rel="noopener">https://www.joyfulbikeshedding.com/blog/2019-03-29-the-status-of-ruby-memory-trimming-and-how-you-can-help-with-testing.html</a>)</li>
<li>HOW THREE GUYS REBUILT THE FOUNDATION OF FACEBOOK (<a href="https://www.wired.com/2013/06/facebook-hhvm-saga/"target="_blank" rel="noopener">https://www.wired.com/2013/06/facebook-hhvm-saga/</a>)</li>
<li>Ruby Memory Environment Variables - Simpler Than They Look. (<a href="http://engineering.appfolio.com/appfolio-engineering/2018/6/27/ruby-memory-environment-variables-simpler-than-they-look"target="_blank" rel="noopener">http://engineering.appfolio.com/appfolio-engineering/2018/6/27/ruby-memory-environment-variables-simpler-than-they-look</a>)</li>
<li>&ldquo;WEAK, STRONG, UNOWNED, OH MY!&rdquo; - A GUIDE TO REFERENCES IN SWIFT (<a href="https://krakendev.io/blog/weak-and-unowned-references-in-swift"target="_blank" rel="noopener">https://krakendev.io/blog/weak-and-unowned-references-in-swift</a>)</li>
</ul>
<p>Olá pessoal, Fabio Akita</p>
<p>Este é o décimo primeiro episódio da série começando aos 40, parte 2 do tema de Gerenciamento de Memória, parte 6 e conclusão do tema de Back-end, finalmente!! Semana passada eu dei uma escovada de bits, expliquei um pouco sobre as diferenças de arquiteturas 64-bits e 32-bits, entendemos rapidamente sobre notação binária e hexadecimal, os diferentes tipos de memória e como os diferentes alocadores fazem seu trabalho pra resolver os problemas de fragmentação e alocação eficiente em ambientes com múltiplas threads reais.</p>
<p>No episódio de hoje vamos uma camada pra cima e explorar mais sobre os famosos garbage collectors, que muita gente ainda associa mais com Java, mas que a maioria das linguagens interpretadas e baseadas em máquinas virtuais possuem. O grande truque é balancear processamento e uso de memória. Se ficar tentando economizar memória, o collector precisa trabalhar mais e, por consequência isso diminui a performance geral da sua aplicação. Por outro lado, se o collector rodar pouco pra aumentar a performance geral, você vai acabar usando mais memória do sistema e em ambientes de servidor onde os processos ficam de pé por dias ou semanas sem descarregar, isso pode ser um problema também. Então, como eles fazem pra balancear os dois?</p>
<p>(&hellip;)</p>
<p>Recapitulando, no episódio anterior falamos um pouco das diferenças de alocadores como o ptmalloc2, tcmalloc do Google e o jemalloc que o Facebook usa. No fim, todo alocador vai querer resolver o problema de fragmentação, o problema de contenção de threads, o problema de cache entre threads. E agora temos os garbage collectors. Pense em garbage collectors como sendo um alocador em user-land. Ele faz syscalls pro sistema operacional, como o malloc() pra pedir blocos grandes de memória e internamente gerencia como esse pedação vai ser usado. Python, Ruby, Erlang, Javascript, .NET, Java e por consequência toda linguagem que roda em cima de Java como Scala, Clojure, Kotlin, usam o mesmo garbage collector da JVM. Assim como schedulers the coroutines em user-land são mais rápidos como eu já expliquei em episódios anteriores, um alocador em user-land também é mais rápido do que fazer syscalls pra funções no kernel-space como mmap ou munmap que é o que funções como malloc() fazem.</p>
<p>Todo garbage collector faz algumas coisas meio básicas. Não dá pra explicar no detalhe como o collector de cada linguagem funciona, mas vamos ver os conceitos comuns. Pra começar, existem linguagens que lidam com ponteiros, ou seja os tais endereços virtuais diretamente, e os que lidam com referências que são como se fossem atalhos pros endereços. Garbage collectors existem principalmente porque nós programadores somos muito ruins em liberar memória manualmente.</p>
<p>Se você teve que lidar com C, sabe que precisa chamar a função free() pra toda memória que deu malloc() antes. Esqueça de dar free e seu programa vai começar a ter leaks ou vazamentos e vai ficar crescendo sem parar. Parece super simples, mas toda regrinha desse tipo você pode ter certeza que vai esquecer. Basta esquecer de digitar uma linha ou apagar uma linha do código com free por acidente. Ou pior, se você der free numa memória que ainda não era pra dar free, vai ficar com um Invalid Pointer por exemplo e seu programa pode crashear. Quando você adiciona classes e objetos em cima do C e gera um C++ a coisa fica pior ainda porque você vai passando ponteiros de objetos pra outros objetos numa árvore gigante de dependências. Quando que você pode destruir um objeto sem deixar outro que dependia dele corrompido?</p>
<p>Uma das formas de resolver esse problema é adicionar a todo objeto um contador. E toda vez que você passar esse objeto pra outro objeto ou função, incrementar o contador. Quanto mais objetos apontarem pra esse primeiro, maior vai ficando o contador. À medida que os objetos que apontam pro primeiro também forem sendo liberados, o contador vai decrementando. E quando o contador voltar a ser zero, daí você sabe que ninguém está mais usando e pode liberar esse objeto. Como estamos falando de contagem de referências, você vai ouvir falar nessa estratégia como Reference Counting. Linguagens como Python e Objective-C e, por consequência, Swift usam essa estratégia.</p>
<p>No caso do Objective-C ele vai um passo além, ele inclui uma classe chamada NSAutoreleasePool, um tanque de alocação. Você instancia um pool e à medida que vai alocando novos objetos esse pool fica sabendo deles. Diferente de um garbage collector, em Objective-C você manualmente instância quantos pools quiser, e tem que manualmente chamar release no objeto que, diferente da tal função free(), vai só decrementando o contador de referência. E quando quiser pode chamar o método release do pool, que por sua vez dá free de verdade nos objetos com contador zerado.</p>
<p>Nas versões mais novas do Objective-C e no Swift, coisa da última década, a Apple adicionou o ARC ou Automatic Reference Counting. Na prática ele vai cuidar de adicionar o código pra dar release do contador usando algumas dicas no seu código, durante a compilação. Vale a pena estudar em detalhes como o ARC funciona se você faz programas pra iPhone ou Mac, mas 90% dos casos você não se preocupa tanto com a memória e vai precisar saber mais quando tiver bugs ou quiser otimizar o uso de memória, por exemplo forçando a limpeza do pool mais vezes. Digamos que você está lendo um arquivo com 500 linhas, em vez de fazer o loop e só no final limpar tudo de uma vez, você pode ir dando release no pool manualmente a cada 50 linhas por exemplo e evitar que o sistema aloque mais memória do que precisa. Se você desaloca memória com muita frequência, vai usar mais CPU e diminuir a performance geral, por outro lado vai consumir menos memória. Se você desalocar com pouca frequência, aumenta a performance, mas também aumenta o uso de memória.</p>
<p>Um problema de contagem de referências é o caso de reference cycle ou seja, você tem objeto A com uma referência pro objeto B daí no B fazemos uma referência pro objeto A, agora ambos os contadores de A e de B nunca vão ser menores que 1, e o pool não vai conseguir desalocar a memória desses objetos mesmo que ninguém mais esteja usando, porque os contadores não são zero. Por isso em Objective-C e Swift você tem a diferença entre strong references que são as referências normais. Mas também temos referências weak e unowned, nenhuma das duas incrementa o contador e isso quebra o ciclo de referências. No caso a referência do objeto B pro objeto A poderia ser declarada como Weak e daí o contador de A chegaria a zero normalmente e ambos poderiam ser limpos. Novamente, você precisa ler em mais detalhes a diferentes de uma referência Weak e Unowned que vou deixar nas descrições abaixo. Linguagens com contadores de referência como Python costumam ter a opção de referências fracas ou Weak.</p>
<p>Controlar os contadores de uso de cada objeto é uma forma de você não precisar manualmente liberar objeto a objeto quando não precisa mais. Basta ter um controlador como o NSAutoreleasePool que consegue limpar todo objeto com contador zerado de uma vez só. Em C++ temos smart pointers que funcionam mais ou menos parecido. Um garbage collector muito simples poderia simplesmente ser uma thread que, de tempos em tempos, manda limpar esse pool. Seria uma forma rudimentar de garbage collector. Mesmo Python trabalha com contador de referências também, mas seu collector é mais sofisticado do que só isso.</p>
<p>Eu falei que em Objective-C você pode criar quantos pools quiser e controla eles manualmente. Outra forma é esse Pool ser tipo uma estrutura global e única, é como funciona em Ruby e em Java e acho que em .NET também. Pense numa lista gigante, onde sempre que precisarmos de memória pedimos pro alocador que vai adicionando nessa lista também, catalogando todos os elementos que vão sendo alocados. Toda vez que você passa a referência desse elemento pra outro objeto, ele vai criando uma árvore de dependência. Objeto A pode ter referências pros objetos B e C, e o objeto C tem referência pro objeto D. Depois de algum tempo, a variável que aponta pra A deixa de existir, por exemplo o método retornou ou você passou nulo pra variável e então ninguém mais aponta pro objeto A.</p>
<p>Com o tempo vários objetos vão ficando nesse estado, onde ninguém mais aponta pra eles a não ser a lista global que falei. Então de tempos em tempos um processo especial chamado collector, numa thread no interpretador ou na VM, poderia ir vasculhando essa lista por todos os objetos que ninguém mais está apontando e ir só marcando. Depois um segundo processo poderia ir na mesma lista, elemento a elemento, desalocando os objetos que foram marcados. Então se a lista tinha 1000 objetos, cada um ocupando 1 slot da memória, na fase de marcação 500 objetos foram marcados como não usados, e na fase de limpeza esses são liberados os 500 slots pra uso. Sendo ultra simplista é isso que chamamos de um garbage collector Mark and Sweep. É um collector de duas fases, onde em cada fase ele precisa vascular elemento a elemento da lista de alocação, fazendo um mark na primeira fase e sweep na segunda.</p>
<p>Porém, quando eu falo desalocar não quer dizer devolver memória pro sistema operacional. Lembra do problema de fragmentação? Se eu ficar devolvendo memória toda hora, muito rapidamente a memória do processo vai ficar fragmentado. Linguagens como Ruby ou Java começam pedindo um certa quantidade de blocos de memória pro sistema operacional. No caso de Ruby tem o equivalente de chunks que ele chama de slots, cada slot tem um tamanho fixo de 40 bytes onde cabe toda a estrutura que define um objeto de Ruby.</p>
<p>Então ele só lida com slots fixos de 40 bytes. Um conjunto desses slots é chamado de Heap. Não confundir com o Heap do malloc de C, é o mesmo nome mas tanto Java quanto Ruby chamam conjuntos de slots ou chunks de Heaps. No caso do Ruby existem várias variáveis de sistema pra configurar o alocador, como por exemplo o RUBY_GC_HEAP_INIT_SLOTS, que por padrão o valor é 1000. Um heap de Ruby costuma ter 16 kbytes. Ou seja, ele vai alocar de 40 bytes x 1000 que seria quase 40 kbytes ou pelo menos 2 heaps.</p>
<p>Se acabar os slots disponíveis, o alocador do Ruby vai pedindo pra alocar mais heaps, vários slots de uma só vez. Digamos que ao carregar seu programa ele precisou de 10 mil heaps, ou seja, uns 156 megabytes. Agora digamos que depois de rodar por um tempo, uns 500 mil slots de objetos foram marcados como não usados mais. Na fase de Sweep você imaginaria que ele vai devolver 500 mil x 40 bytes que dá quase 20 megabytes, de volta pro sistema e em vez de ocupar 156 megabytes ele vai passar a usar 136 megabytes. Mas não, no final do Sweep seu programa vai continuar usando os mesmos 156 megabytes. Por que? Porque ele só vai liberar os slots da lista global, mas a memória de verdade continua alocada. Quando você criar novos objetos nos slots livres, ele vai reusar e escrever em cima do mesmo trecho de memória de antes.</p>
<p>De cara temos estratégias diferentes entre o Heap de uma linguagem como Ruby  ou o Heap de uma linguagem como Java. No caso de Java, todos os objetos ocupam espaço no Heap. No caso de Ruby só a estrutura e dados muito pequenos ocupam os slots de 40 bytes. Mas um array ou um string costuma ser muito maior que 40 bytes, então o Ruby chama a syscall malloc do sistema operacional e guarda o endereço alocado no slots de 40 bytes apontando pro espaço fora do Heap. A vantagem do Ruby é que todo objeto tem o mesmo tamanho dentro do Heap e isso ajuda a não fragmentar tanto, mas ele depende do alocador do sistema operacional pros blobs de dados maiores. No caso do Java ele não tem que se preocupar com o sistema operacional, porque ele usa só sua memória interna do Heap, mas outro lado isso vai causar fragmentação interna e ele tem uma forma de evitar isso, que vamos ver já já. Mas só pra entender que diferentes collectors organizam seus dados internamente de formas diferentes.</p>
<p>Do ponto de vista de programação, se você ainda não entendeu, alocar memória desnecessariamente vai aumentar o tamanho dos heaps ou a quantidade de heaps, dependendo do alocador, e isso vai bloquear memória do sistema operacional e causar fragmentação desnecessária também. Você não pode achar que vai alocar 1 giga de dados, devolver esses 1 giga, e vai continuar tendo bonitinho 1 giga de novo sobrando. Você vai fazer o alocador ter que trabalhar muito pra evitar a fragmentação e se seu sistema tiver pouca memória geral, provavelmente não vai conseguir ter esse espaço livre de volta.</p>
<p>Exemplos de má programação é o famoso caso de abrir um arquivo gigante de vários gigas na memória, ou conectar num banco de dados e não fazer o select com os critérios de where certos e acabar puxando muito mais linhas da tabela do que precisava. Tudo isso vai ocupar espaço, o alocador vai ter que esticar os heaps, mais memória do sistema vai ser consumido, e não necessariamente ele vai liberar essa memória de volta pro sistema. Mesmo depois que o collector limpar os slots ocupados por esses dados, você ficou com um heap inchado e fragmentado. E se seu servidor for pequeno, você vai começar a ter problemas de memória mesmo que teoricamente tenha “memória sobrando” ainda.</p>
<p>Se seu programa rodando em produção tem leaks de memória, normalmente são dados que vem da rede, como dados do banco de dados, ou processamento de arquivos grandes. O ideal é sempre seu código tentar usar memória até um limite. E quando precisa puxar dados externos, ter algumas checagens pra ver se não vai usar memória desnecessariamente. Tipo checar o tamanho de um arquivo antes de carregar ele inteiro na memória. Se for arquivos pequenos de alguns kbytes, dá pra ler tudo e armazenar numa variável de string. Mas se for um arquivo de gigabytes, é melhor abrir como um Stream e ir lendo pedaços do arquivo de cada vez. Ou quando precisa puxar dados de uma tabela num banco de dados, usar coisas como um limit no select pra limitar quantas linhas vai puxar de uma só vez. Famoso caso que o amador fala que na máquina dele funciona, só que na máquina dele ele só testou com arquivos pequenos e com tabelas com 10 linhas de teste. Por isso quando vai pra produção tem esse comportamento. Isso porque é um programador que não tem noção de gerenciamento de memória.</p>
<p>Voltando, outro problema é se seu programa é grande e aloca milhões de objetos. Toda vez que o processo de Mark e Sweep rodar, eles precisam passar por todos eles, um por um, toda vez. Mesmo que só leve uma fração de milissegundo pra marcar e sweepar cada objeto, ainda assim vai levar dezenas de segundos pro processo todo se contar milhões de objetos alocados. É muita coisa. Pior ainda, toda vez que você vai marcar os objetos ou limpar, precisa pausar o programa todo, é o que chamamos de Stop the World. Isso porque você tem que garantir que o programa rodando ao mesmo tempo que o collector não se corrompa tentando acessar um objeto que acabou de ser desalocado. Só que se você pausar seu programa por muitos segundos, ele vai ficar muito lento.</p>
<p>Ou seja, carregar dados desnecessários não só desperdiça memória como desperdiça performance. Agora você só tem 2 escolhas. A primeira é rodar poucas vezes o Mark e Sweep, mas aí você arrisca consumir muita memória e fazer seu heap crescer demais já que sempre que precisa de mais memória e não tem slots pra reusar ele vai pedir mais pro sistema operacional. E eu já disse que toda memória que ele aloca do sistema ele não devolve mais ou vai demorar pra devolver pra evitar fragmentação, daí seu processo vai crescer mais do que devia. A segunda escolha, se você quer evitar isso então tem que rodar mais vezes o Mark e Sweep só que daí a performance geral do programa vai ficar lento. Ou seja, agora você tem que escolher entre performance ou uso de memória.</p>
<p>Pra resolver esse problema é que existe a idéia de Generational Garbage Collectors. Ele vai dividir a memória interna do interpretador ou da máquina virtual em 2 ou mais gerações. Java divide seus heaps em grupos chamados Young, Old e Permanent (por isso você vira e mexe vê configuração em tutorial de JVM pra configurar coisas como o Permsize que é o tamanho desse espaço permanente). O garbage collector G1GC novo do Java chama de Eden, Survivor e Old. Python chama só de geração zero, um e dois.</p>
<p>A idéia vem de uma hipótese chamada Weak Generational Hypothesis. Em linhas gerais essa hipótese diz que objetos novos tendem a morrer jovens e objetos velhos tendem a ficar ativos por muito tempo. Parece óbvio quando falamos assim, né? Eu já expliquei como o sistema operacional divide a memória em arenas por exemplo, pra evitar contenção de threads. Agora vamos dividir mais ainda pra evitar passagens desnecessárias por objetos que não vão morrer tão cedo.</p>
<p>O problema que um generational garbage collector quer resolver é diminuir ao máximo as pausas da fase de mark and sweep. A forma de fazer isso é evitar ficar olhando objetos que nunca vão ser limpos. Por exemplo, objetos singleton que seu programa carrega no começo e vai usar até morrer. Num Ruby, toda classe é um objeto. Mas as classes raramente precisam ser limpas, então pra que perder tempo olhando pra elas toda vez? Em resumo, na primeira vez que o mark passar, ele vai ter que olhar tudo. Objetos que sobrevivem a múltiplas passagens da fase de mark podem ser promovidos da geração Nova pra geração Velha e se sobreviverem mais algumas passadas de mark, podem ser promovidas pra geração permanente.</p>
<p>Podemos dividir as fases de Mark em Minor e Major. O Minor só olha pros objetos mais novos e ativos na geração Nova. O Major olha também pra geração Velha pra ver se algum precisa ser limpo. Mas uma ver promovido pra permanente, podemos esquecer desses objetos. Digamos que um programa normal tenha 100 mil objetos, e que depois de algumas passadas da fase de Mark 50 mil deles tenham sido promovidos pra geração permanente. Os outros 50 mil são objetos que morrem, e novos que são criados e na média ficam sempre uns 20 mil na geração nova, uns 30 mil na geração velha. A performance total da fase de mark é pelo menos o dobro, porque você no mínimo vai passar por metade dos objetos toda vez. E tende a ser mais rápido que isso porque você passa menos vezes na geração velha também. Então é um ganho enorme de performance.</p>
<p>Numa linguagem como Ruby ou Python, esses segmentos como eu disse são só listas com ponteiros pros slots ocupados. Como por baixo é tudo estrutura em C temos endereços e como as extensões em C podem precisar do endereço exato de determinada estrutura não podemos mover os dados de lugar.</p>
<p>Eu menciono isso porque em Java temos uma coisa extra. Java não lida diretamente com ponteiros, ele trabalha com referências no nível da linguagem. Uma variável em Java aponta pra uma referência e a referência, por baixo dos panos, é indexada num endereço. Mas o endereço pode mudar. Diferente de Ruby ou Python, em baixo nível o alocador pode mover o objeto de um slot pra outro nos heaps. Mover fisicamente vai mudar o endereço e aí eu mudo na referência. Mas no alto nível a variável no Java só vê a referência então pra ele nada mudou. Uma coisa é eu dar um produto na mão de uma pessoa. Agora não posso mais tirar o produto da mão dela sem ela reclamar. Outra coisa é eu dar um vale produto pra pessoa. Agora no estoque tanto faz se eu mudar o produto de lugar, o que vale é a referência.</p>
<p>Isso é importante porque na JVM quando acontece a fase de promover os objetos da geração jovem pra geração velha, em vez de só mover o ponteiro de um segmento pra outro de uma lista, na verdade eu estou movendo o objeto de um slot da geração jovem pra um slot da geração velha, fisicamente falando. Estou copiando de um slot pro outro e depois liberando o primeiro slot. Então fisicamente na memória, os heaps de Java tendem a ser menos fragmentados que num Python ou Ruby.</p>
<p>E os objetos de Java podem alocar tudo dentro do objeto em vez de usar a estratégia de dividir o objeto em 2 partes, sendo uma estrutura de tamanho fixo e o resto dos dados alocados fora dos heaps via malloc do sistema operacional. Nesse caso os objetos de Java tem tamanho variável, o que causaria fragmentação como eu já falei mas como ele pode mover os objetos de lugar e ir defragmentando a cada passada do collector, então evitamos fragmentação. É como se o collector do Java fosse também ao mesmo tempo um defragmentador e de fato ele tem uma fase chamada compaction, que move objetos de lugar pra defragmentar se o coletor perceber que o heap acumulou muita fragmentação.</p>
<p>Se não ficou claro, nos diagramas que mostrei até agora, parece que todos os objetos estão movendo de um espaço de memória pra outro. Isso é verdade no Java. Mas num Ruby os objetos não se movem de lugar, os endereços são fixos. Faz de conta que existam listas como uma lista ligada ou um array grande. Cada elemento da lista é um endereço pra memória real, e as listas são as gerações. Então quando eu falo que o objeto é promovido de uma geração pra outra, na memória real nada acontece, só o endereço que sai de uma lista e vai pra outra lista. Num Java, a estratégia de gerações a ajuda a diminuir fragmentação porque à medida que os objetos são promovidos eles movem de lugar na memória e isso vai defragmentando. Mas num Ruby a fragmentação se mantém a mesma, o que vai melhorar é a velocidade do Mark and Sweep.</p>
<p>Esse tipo de processo é o que chamamos de um Compact Copying Collector. A principal função é diminuir a fragmentação da memória. Se você tinha curiosidade, os garbage collectors do Erlang e do .NET são similares. Eles são generational, e tem copy collectors. Os princípios são os mesmos. Só linguagens que lidam com ponteiros e endereços físicos diretamente como as linguagens que compilam nativamente sem um runtime que abstrai a memória ou os interpretadores, não tem como fazer compact ou copy e tendem a fragmentar mais. Não quer dizer que é impossível fazer a parte de copy. Se você mudar um objeto de lugar, ele ganha um novo endereço, agora você precisa garantir que quem tente acessar o endereço antigo caia no novo, algo como uma barreira de leitura. Um dos meus problemas com acesso a ponteiros não é só por aritmética de ponteiro, mas porque dificulta defragmentação via compact copying. Era o que o Go pretendia fazer, mas se não me engano até agora eles não implementaram a fase de copy.</p>
<p>Além disso podemos executar partes dessas fases concorrentemente e paralelamente usando threads reais. Por isso em JVM você ouve falar de nomes como Parallel GC ou ConcurrentMarkSweep GC. O primeiro usa múltiplas threads. O segundo só uma thread, mas ele pausa o mundo só na fase inicial de mark e sweep, mas depois ele roda paralelamente à aplicação. JVM pra servidor usava o Parallel GC por exemplo. Hoje em dia temos o G1GC que usa gerações menores de segmentos 0, 1 e 2 ou Eden, Survivor e Old como expliquei acima, mas ele usa múltiplos segmentos de cada geração em vez de segmentos contíguos e outros algoritmos.</p>
<p>O problema de rodar concorrentemente é a fase de Mark and Sweep, que exige a tal pausa do programa ou Stop the World. Pra evitar isso, linguagens como Erlang implementam uma versão melhor que é mais custosa mas evita a pausa, chamada Tracing collector. Pense uma fase de mark dividida em 3 fases, como se fossem gerações, usando alguma variação do algoritmo chamado Tri-color, onde determinamos cores como preto, cinza e branco e no mark os objetos são movidos pra preto ou branco, e no final quando nenhum objeto for cinza, podemos fazer sweep de todos os objetos que sobraram com a cor branca. A vantagem disso é que podemos fazer essa marcação sem precisar pausar o programa. O Incremental Mark and Sweep do Ruby usa estratégia de Tri-color pro que se chama de Minor GCs ou fases de mark mais rápidas.</p>
<p>O collector do Java tem vários outras estratégias além do que expliquei até agora. De qualquer forma, o que chamamos de um garbage collector, completo, poderia ser um “Parallel Concurrent Generational Incremental Mark and Sweep Copy Compactor Collector”. Ruby e Python fazem algo similar, com a limitação de não poderem ser copy collectors nem rodar collectors concorrentemente. Javascript faz a mesma coisa também.</p>
<p>Generational garbage collectors tendem a usar mais memória pra manter todas as estruturas e segmentos pra cada geração. Ou seja, é um overhead mais alto do que o jeito do Objective-C e Swift. Linguagens compiladas nativamente com um runtime pequeno como C ou Objective-C tendem a depender principalmente do alocador do sistema operacional como o ptmalloc2 ou jemalloc. Linguagens interpretadas são híbridos, ele tem um espaço de heaps alocados com alocador em user-land pra gerenciar os objetos mas usam o alocador do sistema operacional pra blobs maiores como blocos de texto ou arrays grandes.</p>
<p>Uma JVM, se não me engano, pede pro sistema operacional alocar um espaço grande no começo do boot, mas depois ele se vira só em user-land. Vai usar mais memória, mas como não depende de syscalls, tende a ser mais rápido. Ao mesmo tempo, linguagens compiladas e interpretadas dependem do alocador do sistema operacional pra gerenciar a fragmentação de memória como já expliquei no episódio anterior. Uma JVM controla a fragmentação ele mesmo via o processo de copy collector onde ele consegue mover os objetos de lugar.</p>
<p>Falando no alocador do sistema operacional, o ptmalloc2 que é o malloc do glibc que a maioria das linguagens usa, aloca arenas pra cada thread real como já expliquei no episódio anterior. Pior ainda, ele não tem thread-cache ainda como o tcmalloc ou jemalloc. Portanto quanto mais arenas ele alocar, mais memória vai desperdiçar. E o multiplicador pra máquinas 32-bits são de 2 vezes o número de threads reais ou seja, máquinas quad-core com hyperthreading que dá 2 threads por core significa pelo menos 16 arenas. Mas em máquinas 64-bits o multiplicador é 8 então são 8 vezes 4 cores vezes 2 threads que dá 64 arenas!! É muita arena. Em estudos do Heroku que é uma plataforma de serviços para deployment de aplicações em ambiente virtualizado eles recomendam diminuir o teto de arenas. Em máquinas pequenas, com 2 cpus virtuais, em testes empíricos ficou notável que configurar o teto pra 2 arenas é mais que suficiente.</p>
<p>Segundo Hongli Lai, CTO da Phusion, o motivo desse multiplicador ser tão alto pode ser porque o principal desenvolvedor do alocador de memória é da Red Hat que vende produtos e serviços pro mundo enterprise. Lá a prioridade é aumentar performance mesmo que isso custe desperdiçar muita RAM. 10% a mais de performance por 30 vezes mais memória é uma conta que o mundo Enteprise topa pagar. Mas pra gente que precisa balancear performance com uso de memória pra escalar, a recomendação é simples, basta exportar a variável MALLOC_ARENA_MAX pra 2 e pronto. Só isso já vai evitar muitos vazamentos de memória por mau uso de arenas.</p>
<p>Gerenciamento de memória é um assunto absurdamente longo. Até este ponto eu expliquei tudo em linhas gerais. Cada parágrafo deste episódio tem uma literatura gigante que você vai precisar procurar pra entender em mais detalhes. Eu falei de copy collectors numa linha, como se fosse algo trivial, mas não é. Eu falei de thread-cache de malloc como se fosse uma correção simples do ptmalloc2 pro tcmalloc, mas não é. Eu falei como o jemalloc é mais rápido que o tcmalloc, mas procure os relatos de Jason Evans pra ver o tanto de trabalho que eles tiveram pra reescrever várias partes, várias vezes, até realmente chegar na versão atual. Alocadores de memória são super fáceis de escrever errado, de um jeito que vai ser super lento e consumir gigabytes de RAM à toa e ficar vazando em servidores. Nunca tente escrever seu próprio alocador a menos que tenha toneladas de dinheiro e tempo pra jogar fora.</p>
<p>Por outro lado, você precisa se interessar em estudar todas as formas que seu alocador funciona. Não basta ver quanto seu processo consome quando sobe. À medida que ele roda o garbage collector vai movendo objetos entre gerações, vai alocando memória do sistema operacional pras suas heaps internas que ele não vai devolver pro sistema operacional ou vai demorar pra devolver. Você precisa achar os pontos no seu programa que geram picos de consumo de memória, porque eles que vão pressionar o tamanho do heap e desperdiçar memória do sistema operacional.</p>
<p>Em última instância, você vai lembrar que todo collector tem dezenas configurações pra fazer tuning. Tamanho inicial do heap, fator de crescimento dos heaps, frequência pra rodar os collectors, quantidade de arenas, etc. Normalmente a configuração que já vem é o suficiente pra 99% das aplicações. Não seja ingênuo de achar que seu programa vai ficar muito melhor fazendo tuning dos alocadores ou garbage collectors, praticamente em todos os casos é seu código que desperdiça memória e tuning nenhum vai resolver isso. Mas no 1% dos casos que você já mediu exaustivamente o comportamento da aplicação em produção com dados reais, já corrigiu todo buraco de código possível, então talvez exista alguma chance de um tuning ajudar um pouco. Foi assim que na comunidade Ruby por exemplo descobrimos sobre o tuning de arenas do ptmalloc2 ou trocar pra jemalloc.</p>
<p>Em ambientes móveis, smartphones, é pior ainda, porque você não tem disco pra fazer swap. Significa que diferente de desktops e servidores, se seus programas foram mal feitos e alocarem memória de pico demais sem desalocar, o OOM ou Out of Memory Killer vai entrar em ação e matar seu programa com muita frequência, tornando a experiência do usuário uma droga. Por isso eu particularmente não gosto das soluções antigas de abrir aplicações que são só casca pra um navegador web e dentro carregar uma aplicação Javascript. No Javascript você não tem controle sobre o garbage collector e não tem acesso ao pool de memória como no num app nativo de iOS onde você pode mexer nos NSAutoreleasePool, significa que você está à mercê de um generational garbage collector, que usa mais memória que um app nativo, vai ter pausas de mark and sweep, e um Just in time compiler que também está alocando memória e usando mais CPU o tempo todo, porque otimizar código não sai de graça. Tudo tem um preço. Quanto mais alguma coisa é conveniente, menos ele tende a ser eficiente.</p>
<p>Quando você está programando pra desktop, que hoje em dia é tudo 64-bits, com muitos endereços virtuais sobrando, mesmo que falte RAM, seu sistema operacional vai jogar tudo pra arquivos de swap em disco. Fica lento, mas nada crasheia, por isso hoje em dia ficou comum fazer todo aplicativo desktop ser uma casca de navegador também e rodar apps web dentro. É o que Spotify, ou Slack ou os editores de texto Atom fazem. Consome memória pra caramba e quanto mais tempo eles ficam rodando mais memória vão consumindo. Por outro lado são super convenientes de fazer e você não precisa controlar memória manualmente. Eu ainda tenho preferência por programas nativos bem feitos que alocam memória de formas eficientes sem a necessidade de usar um garbage collector custoso. Um garbage collector é conveniente, mas não é necessário, especialmente em programas desktop.</p>
<p>Como eu já disse antes, tudo é uma questão de trade-off, você troca conveniência por mais lentidão e mais memória. Quanto menos conveniente for, mais você ganha performance e usa menos memória. Não existe um vencedor óbvio, cada caso é um caso. É um aplicativo pra servidor? Pra desktop? Pra smartphone? É grande? É pequeno? Precisa trabalhar quantidades grandes de dados com muita frequência? Precisa coordenar o processamento desses dados paralelamente? Ou de forma distribuída? Tudo isso afeta o que você vai escolher.</p>
<p>Com isso você também deve entender porque eu disse no episódio passado que linguagens com máquinas virtuais como Java ou Erlang preferem tomar conta de todos os recursos da máquina, porque o ideal é deixar eles alocarem toda a memória disponível pra gerenciarem os recursos internamente com seus garbage collectors. Percebam que eles levam pra user-land todas as atribuições do sistema operacional no kernel space. Eles fazem poucas syscalls porque conseguem gerenciar coisas como corotinas e gerenciar memória tudo em user-land.</p>
<p>O Go usa as funções de alocação como um malloc faz e prefere ficar mais baixo nível, mas ao mesmo tempo evitando depender demais do sistema operacional. Ele evita um pouco da fragmentação porque divide a memória em slabs dependendo do tamanho dos chunks como o ptmalloc e outros mallocs fazem. Mas diferente de Java e Erlang ele não tem e vai ter trabalho pra implementar um copy collector porque expõe ponteiros. É o mesmo motivo de porque Python ou Ruby também Não tem copy collectors. Generalizando um pouco, linguagens interpretadas ou com máquinas virtuais tendem a usar mais memória. Linguagens que não expõe ponteiros podem implementar compact copying collectors e evitar mais fragmentação ao custo de usar mais processamento.</p>
<p>E finalmente, chegamos ao último episódio do tema de Back-end! Gerenciamento de Memória era o último assunto pra vocês finalmente terem mais entendimento do funcionamento da máquina e sistema operacional. A discussão sobre linguagens só faz sentido se você entende essas coisas, senão a discussão fica mais parecendo fofoca de telenovela, sem argumentos e só repetindo frases de efeito que alguém disse num tweet ou blog post. Todo mundo só sabe até o ponto que a linguagem tem ou não garbage collector. E todo mundo só acredita que ter um generational garbage collector é bom. Mas ninguém sabe explicar porque é bom e o que você paga tem troca.</p>
<p>Mais do que isso, sem entender concorrência, paralelismo e gerenciamento de memória, você também não consegue entrar direito em devops e lá também vai ficar só seguindo superstições, mandingas, e repetindo o que ouviu os outros falar sem realmente saber o que está fazendo. Entender como a máquina e o sistema operacional realmente funcionam, pra mim, é o que começa a dividir as crianças dos adultos.</p>
<p>Se você tem dúvida mande nos comentários abaixo, se curtiu mande um joinha, compartilhe com seus amigos, não esqueça de assinar o canal e clicar no sininho pra não perder os próximos episódios. A gente se vê semana que vem, até mais.</p>
]]></content:encoded><category>performance</category><category>linguagens-de-programacao</category><category>ciencia-da-computacao</category><category>akitando</category></item><item><title>[Akitando] #45 - Gerenciamento de Memória (Parte 1) | Entendendo Back-end para Iniciantes (Parte 5)</title><link>https://akitaonrails.github.io/2019/03/27/akitando-45-gerenciamento-de-memoria-parte-1-entendendo-back-end-para-iniciantes-parte-5/</link><guid isPermaLink="true">https://akitaonrails.github.io/2019/03/27/akitando-45-gerenciamento-de-memoria-parte-1-entendendo-back-end-para-iniciantes-parte-5/</guid><pubDate>Wed, 27 Mar 2019 17:01:00 -0300</pubDate><description>&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/9AK_1gqEfkQ"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;Eu disse no episódio anterior que não sabia se ia falar sobre Gerenciamento de Memória, mas acho que não dá pra terminar a série e não falar disso. Então hoje vamos escovar bits um pouco. Como a série é pra iniciantes, vale revisitar um pouco sobre representações binárias e hexadecimal e entender mais sobre como o computador e seus programas enxergam essa tal de &amp;ldquo;memória&amp;rdquo;.&lt;/p&gt;</description><content:encoded><![CDATA[

<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/9AK_1gqEfkQ"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Eu disse no episódio anterior que não sabia se ia falar sobre Gerenciamento de Memória, mas acho que não dá pra terminar a série e não falar disso. Então hoje vamos escovar bits um pouco. Como a série é pra iniciantes, vale revisitar um pouco sobre representações binárias e hexadecimal e entender mais sobre como o computador e seus programas enxergam essa tal de &ldquo;memória&rdquo;.</p>
<p>Hoje quero explicar como os principais alocadores de memória do Linux funcionam e ensinar o que são os principais desafios que envolvem o gerenciamento de memória. Tudo isso vai ser importante pra semana que vem que vou explicar sobre garbage collectors.</p>
<p>Erratas:</p>
<ul>
<li>
<p>aos 11:39 min o áudio fica ruim por 1 minuto. Eu gravo o áudio num gravador externo e mantenho o áudio da câmera pra sincronizar. Mas meu gravador parou nesse minuto e fiquei só com o áudio da câmera, por isso que a qualidade cai, mas é só 1 minuto.</p>
</li>
<li>
<p>a 26:50 min aparece um typo &ldquo;Stacjk&rdquo; que na verdade é &ldquo;Stack&rdquo;</p>
</li>
</ul>
<p>Links</p>
<ul>
<li>The 640K memory limit of MS-DOS (<a href="https://www.xtof.info/blog/?p=985"target="_blank" rel="noopener">https://www.xtof.info/blog/?p=985</a>)</li>
<li>The difference between booting MBR and GPT with GRUB (<a href="https://www.anchor.com.au/blog/2012/10/the-difference-between-booting-mbr-and-gpt-with-grub/"target="_blank" rel="noopener">https://www.anchor.com.au/blog/2012/10/the-difference-between-booting-mbr-and-gpt-with-grub/</a>)</li>
<li>How Linux system boots up? (<a href="https://medium.com/@garimajdamani/how-linux-system-boots-up-f15c9e0f7a96"target="_blank" rel="noopener">https://medium.com/@garimajdamani/how-linux-system-boots-up-f15c9e0f7a96</a>)</li>
<li>Rebasing Win32 DLLs (<a href="http://www.drdobbs.com/rebasing-win32-dlls/184416272"target="_blank" rel="noopener">http://www.drdobbs.com/rebasing-win32-dlls/184416272</a>)</li>
<li>Commodore 64 memory map (<a href="http://www.awsm.de/mem64/"target="_blank" rel="noopener">http://www.awsm.de/mem64/</a>)</li>
<li>Physical Address Extension - PAE Memory and Windows (<a href="https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn613975%28v=vs.85%29"target="_blank" rel="noopener">https://docs.microsoft.com/en-us/previous-versions/windows/hardware/design/dn613975(v=vs.85)</a></li>
<li>A visual guide to Go Memory Allocator from scratch (Golang) (<a href="https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed"target="_blank" rel="noopener">https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed</a>)</li>
<li>Scalable memory allocation using jemalloc (<a href="https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919"target="_blank" rel="noopener">https://www.facebook.com/notes/facebook-engineering/scalable-memory-allocation-using-jemalloc/480222803919</a>)</li>
<li>Understanding glibc malloc (<a href="https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/"target="_blank" rel="noopener">https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/</a>)</li>
</ul>
<p>Olá pessoal, Fabio Akita</p>
<p>Finalmente, estamos pertos de concluir o tema de Back-end dessa série Começando aos 40. Já estamos no décimo episódio pra falar sobre Gerenciamento de Memória. Mas diferente dos episódios passados que estavam dando quase 1 hora de duração, este eu resolvi quebrar em duas partes. Tá bem trabalho editar esse tanto de vídeo toda semana, e mesmo assim vai ser comprido hoje, então prestem atenção!</p>
<p>Durante os últimos episódios eu meio que dei uma introdução a coisas que vocês aprenderiam em  matérias como de sistemas operacionais numa faculdade de Ciências da Computação, então espero que aproveitem os tópicos que eu mencionei pra se aprofundar mais nos estudos desses temas.</p>
<p>Vou dizer que fiquei em dúvidas se deveria ou não me estender no tema de hoje, ele é ainda mais escovação de bits do que os temas anteriores. Mas eu sinto que é outro assunto que a grande maioria dos programadores, não só os iniciantes, até hoje não entendem direito. E o assunto é gerenciamento de memória. Todo mundo acha que porque suas linguagens costumam ter coisas como garbage collector, então é um assunto irrelevante. Mas isso não é verdade. Um programador amador realmente não vai prestar atenção nisso e em várias outras coisas, mas se você realmente tem a intenção de ser um verdadeiro profissional então sim, você precisa dominar esse assunto.</p>
<p>(&hellip;)</p>
<p>Recapitulando, eu já expliquei várias vezes como seu código fonte compila num binário e esse binário é carregado em memória. Também já rapidamente expliquei como CPUs e sistemas operacionais modernos alocam espaços de memória isolados pra cada processo, com endereços virtuais de tal forma que um processo não tem como acessar a memória de outro processo. Como eu já disse antes, se você realmente pretende levar a sério a profissão, precisa entender como seu computador realmente funciona.</p>
<p>Uma coisa que eu nunca mencionei por exemplo é o processo de boot em si, que é bem fascinante. Em hiper resumo, tem diversas etapas, do momento que você aperta o botão de ligar, segue o processo de Power on self test ou POST que você vai ouvir falar bastante se acompanha canais de montar seu computador, tem o teste de memória e outros dispositivos, até chegar no Master Boot Record que é o antigo MBR ou o atual GUID Partition Table ou GPT pra carregar o boot loader, que no caso do Linux seria um GRUB2 por exemplo, e finalmente carregar o binário do Kernel propriamente dito que, por sua vez, vai carregar um systemd, que por sua vez finalmente vai carregar todos os daemons pra terminar de tornar disponível os recursos da máquina pros programas, como montar suas partições do disco.  Se você está estudando Linux, veja os links que deixei na descrição abaixo que detalham esse processo em mais detalhes.</p>
<p>Eu estava pensando em quanto de detalhes quero mostrar hoje, mas se eu for realmente minucioso o episódio vai ficar com horas e horas de duração e vai ser extremamente tedioso. Em vez disso vou delinear os pontos principais e explicar resumidamente, você precisa pesquisar mais detalhes depois nos seus estudos.</p>
<p>Em termos de memória, entenda que você tem os pentes de RAM, o que você normalmente vê como pente de 16GB DDR4, DDR que significa Double Data Rate. De curiosidade eu já falei rapidamente antes que tudo no computador funciona segundo o clock da CPU. Sabe quando a gente fala em CPU de 4 Ghz? Sendo hertz um ciclo, 4 Ghz significa 4 bilhões de ciclos por segundo, pense em clock literalmente como um ciclo ou uma ida e volta de um pendulo, uma frequencia. Double Data Rate significa que é possível puxar dados da memória no topo do ciclo e na parte de baixo do ciclo, em vez de só conseguir puxar dados uma vez por ciclo como era antigamente, então ele tem a chance de puxar duas vezes, por isso é mais rápido, mas hoje em dia a gente só usa DDR de qualquer forma.  Uma diferença pra memórias de servidor são as memórias ECC que significa Error Correcting Code. É uma memória bem mais cara mas que garante que a memória nunca vai ser corrompida, pentes de RAM normais como as que usamos tem chances de devolver 1 bit errado de tempos em tempos, mas é raro, memória ECC de servidor tem uma proteção extra e menos chances.</p>
<p>O que o seu simples programa vê não é só RAM, é mais um “espaço de memória”, a CPU vai cuidar do que vai aonde. Você tem caches L1, L2 e L3 que são memórias pequenas e hiper rápidas que ficam no mesmo chip, pra fora dele você tem um barramento, que vai conectar com dispositivos como os pentes de RAM e com seu HD mecânico ou SSD que também pode servir de memória se conter um arquivo de SWAP. Se você não sabia disso memória é dividida normalmente entre acesso rápido e devagar. L1, L2 e L3 são memórias ultra rápidas. Quanto mais rápido mais caro, lógico, por isso eles são muito pequenos da ordem de até 64 quilobytes de L1, até meio megabyte de L2 e até 8 megabytes de L3. Eles não são memórias pra você usar, o CPU vai usar principalmente como cache pra manter seus pipelines cheios. Depois disso vem a RAM propriamente dita, seus pentes de DDR4, ordens de grandeza mais lentos que os caches, mas de uso geral. E depois vem coisas como o SWAP do seu SSD que é ordens de grandeza mais lento que a RAM.</p>
<p>Eu gostaria muito de explicar em detalhes o que são portas lógicas, como se chega num flip-flop, e como flip-flops podem ser usados pra armazenar bits. Pra ter um modelo na cabeça, pense em memória como um livro, com um espaço reservado pra um índice, e cada linha apontando pra uma página desse livro. Essas linhas são as linhas de endereço. Vamos entender essas linhas de endereço e como elas são organizadas.</p>
<p>Quando falamos que uma CPU é de 64-bits significa que os tais registradores da CPU acomodam números de até 64-bits de tamanho, e além disso temos barramentos, ou seja as rodovias de comunicação entre a CPU e a memória e dispositivos de I/O que também tem um tamanho máximo. 64-bits significa que a CPU consegue lidar com words ou palavras de 64-bits inteiros, que equivale a 2 elevado a 64 que seria o absurdo número de 16 Exabytes.  Pense assim, você tem bytes, quilobytes, megabytes, gigabytes, terabytes, petabytes e exabytes. 16 Exabytes é este numerazão aqui embaixo se for em bytes (18.446.744.073.709.551.616).</p>
<p>Agora, esse numerozão está representado no sistema decimal, que é o que estamos mais acostumados a lidar, mas quando falamos em computador, ele representa tudo em 1 ou 0. Vamos escovar bits um pouco. O número 1 é só 1, o número 2 é 1-0. O número 11 é 1-0-1-1. Pense em binário como a tabuada do 2, o número mais pra direita é 1, o segundo da direita pra esquerda é 2, na sequência vem 4 e depois 8 então 1011 é 8 + 0 + 2 + 1 que é 11, entenderam? Portanto o menor número de 64 bits é basicamente 64 zeros em sequência e o maior número seria 64 1s em sequência.</p>
<p>Obviamente ficar representando tudo em binário no papel é muito longo. Como lidamos com bytes que são conjuntos de 8 bits, é mais fácil lidar com uma representação que é múltipla de 8 ou, melhor ainda, múltipla de 16, que é o que chamamos de hexadecimal. É um sistema que em vez do nosso decimal que vai só até 10 ele vai até 16, mas como números arábicos só tem caracteres de 0 a 9, precisamos usar as letras do alfabeto de A até F pra ir até 16. O mesmo número máximo de 64 bits que eu falei que é 64 vezes o caracter “1”, em hexa seria quatro grupos de quatro Fs.</p>
<p>Tão vendo? Aquele numerozão horroroso em decimal que você jamais vai decorar é muito mais simples em hexadecimal e você consegue literalmente saber de cabeça que vai de 0 até 16 Fs. É muito mais curto. E em binário você tem propriedades matemáticas interessantes, por exemplo, multiplicar um número binário por 2, é simplesmente um shift left, tipo mover tudo uma casa pra esquerda e colocar um zero. Por exemplo o número 11 que eu falei que é 1-0-1-1 vira 1-0-1-1-0 que é 16 + 0 + 4 + 2 + 0 que é 22. Algumas coisas ficam mais simples de calcular.</p>
<p>Você precisa entender que usamos o sistema decimal que vai de 1 até 10 por motivos históricos. Vale pesquisar a história da matemática, você vai encontrar outros sistemas. Um exemplo, se você usar o relógio como parâmetro, é mais fácil contar de 1 até 60. Computadores são 1 ou 0 porque é o sistema mecânico mais simples, pense o interruptor de luz da sua casa, em todo instante ele armazena uma de duas informações: ligado ou desligado. 10 interruptores guardam 10 bits de informação. E os computadores mais rudimentares do passado usavam lâmpadas a vácuo, como o que ilumina seu quarto neste instante. Luz ligada ou desligada. Coloque 10 lâmpadas e você pode visualizar 10 bits de informação.</p>
<p>Continue estudando mais sobre representação binária e hexadecimal, é importante você ter essas ordens de grandeza na cabeça de forma natural, mesmo que seu trabalho não seja fazer aritmética direto nesses sistemas. De qualquer forma, estávamos falando sobre computadores 64-bits. Este número abaixo (16 Fs) é o maior número inteiro que é possível armazenar em uma palavra de 64-bits e voltando às linhas do índice do nosso livro, significa que cada linha de endereço pode apontar números só até esse tamanho e, por consequência, esse tamanhão máximo de RAM ou páginas do nosso livro. Só que isso é teórico.</p>
<p>Nenhuma máquina que eu saiba tem capacidade pra realmente comportar esse tanto de memória real. Um dos motivos é porque embora o processador tenha registradores de 64-bits, o barramento entre a CPU e a memória em processadores Intel, se não me engano, é de 42-bits, e na AMD é de 48-bits, portanto o máximo endereçável é 2 elevado a 48 que seria 256 Terabytes. 2-bits fazem muita diferença, porque é o quadrado do quadrado, então na Intel você conseguiria endereçar no máximo 4 Terabytes.</p>
<p>Isso é ordens de grandeza maior do que em computadores 32-bits da geração passada. E não, 64 não é o dobro de 32-bits, é o quadrado. Lembrando que 32-bits significa 2 elevado a 32, que limita o endereçamento a no máximo 4GB de RAM. Se você tinha computador nos anos 2000 vai lembrar que não dava pra colocar mais que isso de RAM. Se você chegou a ter acesso a servidores com Windows Server 2003 Service Pack 2 Datacenter Edition dava pra ter até 64 GB.</p>
<p>Isso porque havia um truque da Intel em processadores 32-bits chamado PAE ou Physical Address Extension, e dependendo da configuração de hardware e sistema operacional, dava pra ir acho que até um máximo de 128 Gigas mesmo num computador 32-bits. Na prática, os processos num sistema desses continuavam só enxergando 4GB mas era possível ter mais processos ativos porque com PAE você podia swapar alguns processos pra essas áreas extendidas de memória e evitar usar swap de disco por exemplo.</p>
<p>Tudo isso dito, vamos tentar explicar mais sobre como o sistema operacional enxerga essa memória. Até agora sabemos que a memória é como um livro, com linhas de endereço no índice, cada endereço representando uma página do livro, com o endereço máximo dependendo se o computador tem barramento de 48-bits, 42-bits, 36-bits, ou 32-bits e assim por diante. O sistema operacional pode ler e escrever em qualquer endereço de memória. Num sistema operacional muito simples, tipo um Basic num Commodore 64, dava pra acessar diretamente esses endereços pra escrever ou ler. Na verdade existe um porém, você poderia achar que seu programa tem acesso a tudo do endereço 0000 até o endereço FFFF por exemplo, num computador de 16-bits. Mas quando você boota um Commodore, apesar dele poder endereçar 64KB, porque tinha barramento de 16-bits, você vai ver que ele diz que só tem 38.911 bytes livres. Isso porque como em qualquer máquina, nem todos os endereços estão livres pro usuário.</p>
<p>Um pedaço dos endereços é reservado pra kernel, outro pedaço são funcionalidades como endereços de I/O, outro pedaço no caso do Commodore é o próprio interpretador Basic, e o que sobra é onde você pode carregar seus programas. O programa Basic só pode carregar a partir do endereço $0801 até o $A000, se você fizer a conta, A000 é o decimal 40,960 e 0801 é o decimal 2049. Então 40,960 menos 2049 são os 38911 bytes que o sistema declara como disponível.</p>
<p>Isso era na época em que tudo usava endereços reais, até o fim dos anos 80, antes dos processadores Intel 80286. Os computadores modernos a partir dos anos 90 tem o tal modo protegido e memória virtual. Quando o sistema operacional carrega seu programa ele dá pra ele um índice virtual, que vai do endereço virtual 0000 0000 até o FFFF FFFF. Só que o endereço virtual 0000 0000 desse processo, na memória real pode ser um endereço nada a ver, faz de conta, DCBA 9876, mas seu processo não tem idéia disso, só o sistema operacional sabe mapear entre os dois.</p>
<p>Daí você carrega outro programa, ele também ganha outro índice virtual que vai de 0000 0000 até FFFF FFFF só que o 0000 0000 desse programa mapeia pro endereço real BA98 7654 por exemplo. Ambos os programas enxergam o 0000 0000 mas cada um aponta pra um lugar diferente. E por isso os dois programas são ditos isolados, porque não importa que endereço eles tentem apontar, nunca vai ser em cima do endereço do outro processo porque o sistema operacional garante que um endereço real usado não é apontado por mais do que um índice virtual.</p>
<p>A grande vantagem disso é que você pode ter 10 programas, cada um “enxergando” 4 GB de memória inteiro só pra ele, ou seja endereços virtuais de 0000 0000 a FFFF FFFF mas seu computador real pode ter bem menos memória que isso. Na prática a maioria dos programas não vai usar toda a memória disponível, então o sistema operacional compartilha a memória real entre os diversos programas até acabar a memória real. Então seu processo tem só a ilusão de que tem toda essa memória. E o que acontece quando acaba 100% da memória real do sistema, ou seja, toda a RAM e todo o swap no HD? Antigamente seu computador ou ia ficar lento ao ponto de ser inusável, ou programas iam começar a crashear por falta de memória, ou alguma combinação disso.</p>
<p>Num sistema operacional de smartphones você já sabe o que acontece. Num iPhone ou qualquer Android moderno, você vai abrindo programas e não precisa fechar. Quando acaba a memória, os programas menos usados ou os abertos à mais tempo e sem uso, fecham sozinhos, dando espaço pra abrir mais programas. Num Android, que é um derivado de Linux, e mesmo iOS que é derivado de BSD UNIX, existe o OOM Killer ou Out of Memory Killer. Todo Linux tem isso, e é isso que evita que seu celular fique sem memória. É por isso que falamos que em celulares modernos você não precisa ficar fechando manualmente os programas, o OOM Killer vai fazer isso por você.</p>
<p>Voltando aos seus processos, o sistema operacional começa dando zero bytes de memória real pro seu processo. À medida que o processo vai pedindo memória, ele vai alocando memória real, sob-demanda, e mapeando os endereços reais pra endereços virtuais que o processo enxerga. E quando o processo vai devolvendo memória ou quando você fecha o programa, esses endereços reais voltam a ficar disponíveis pra outros processos poderem usar.  Se você simplesmente abre programas de Linux como ps, top ou htop ou o Task Manager no Windows, vai quanto cada programa está alocando. Pra simplificar digamos que você tem 2 programas, cada programa você vê ocupando 1.5 GB. Ou seja, cabe tudo em 3 GB de RAM. Daí na sua cabeça você acha que porque tem 4 GB de RAM, tem memória de sobra, certo? Errado. Um desses programas pode estar muito perto de crashear.</p>
<p>Pra simplificar, vamos considerar ainda um computador de 32-bits nesse exemplo. Eu disse que seu programa enxerga do endereço virtual 0000 0000 até FFFF FFFF. Mas ele Não vai poder usar tudo isso. Num Windows 32-bits de mais de 10 anos atrás, você começa tendo só 2 GB disponível, do começo até a metade, ou seja de 0000 0000 até 7FFF FFFF. Daí pra cima é reservado ao sistema operacional. Lembra como eu falei no Commodore 64 que partes dos endereços são reservados pro Kernel, Basic, I/O e outras coisas? Mesmo coisa em sistemas operacionais modernos. Mesmo num Linux, ele vai reservar de 512MB a 1 GB de endereços. Num Windows Server você podia bootar com um flag especial e compilar os programas com a flag /largeaddressspace pra permitir usar um pouco mais além da metade dos endereços. Não quer dizer que o Windows está realmente usando 2 GB, mas sim que os endereços estão reservados pra ele usar.</p>
<p>Ou seja, num computador 32-bits, apesar de poder mapear até 4GB e mesmo se você tivesse 4GB reais de RAM, nenhum processo seu iria conseguir usar mais que 2GB. Hoje em dia você tem problemas de memória em computadores 64-bits? Imagina quando a gente tinha que programar servidores em 32-bits. Pior ainda, imagina como era na época dos 16-bits. Saber usar memória é até hoje o que diferencia um amador de um profissional de verdade. Pra dar um exemplo, lembram do episódio que eu falei de SAP? Nessa época, acho que era 2003, eles tinham um servidor de aplicações Java, o In-Q-My, ainda em beta, pra carregar aplicações J2EE pesadas como o Enterprise Portals. No boot do Java com os programas SAP por cima, ele carregava tranquilamente um giga e meio.</p>
<p>Se você fosse amador, pensaria, “ah, sussa, máquinas 32-bits carregam até 4GB então tem sobrando”. Se você fosse intermediário pensaria “puta que pariu, só mais 500 Mb e vai bater no limite de 2 GB”. Porém, eu não conseguia fazer esse servidor subir, ele crasheava por falta de memória. Mas eu descobri porque. Lembram que eu falei que o sistema operacional usa parte da memória do processo? Seus programas reusam muitas bibliotecas que o sistema operacional compartilha com os processos, então você tem endereços virtuais que mapeiam pros endereços reais dessas bibliotecas do sistema, incluindo muitas DLLs que todos os processos usam como a LibC da Microsoft ou o Foundation Classes. Pra você entender, o sistema não está duplicando essas bibliotecas em todos os processos, mas os endereços virtuais dos processoa mapeiam pras mesmas bibliotecas. Os processos são isolados, então pra eles enxergarem alguma coisa de fora, os endereços virtuais precisam mapear pra esses recursos compartilhados.</p>
<p>Ainda pensando de forma ingênua, você imaginaria que essa bibliotecas carregam bonitinhas sequencialmente uma atrás da outra, sem desperdiçar nenhum endereço. Lógico que não, não necessariamente. E de fato, algumas dessas bibliotecas apareciam com endereços na primeira metade dos endereços virtuais do processo, ou seja, em cima dos 2 GB reservado pro seu programa. Isso se chama fragmentação de memória. A JVM precisava alocar memória contígua no boot, ou seja com endereços sequenciais, sem buracos no meio. Aliás, alocação de memória sempre aloca pedaços inteiros, sequenciais. Como a memória estava fragmentada, não havia um trecho sequencial limpo de 1.5 GB. E agora?</p>
<p>Felizmente, no Windows você tem como fazer rebase das DLLs, pense mais ou menos, como defragmentar a memória. Exatamente isso, você pode mudar o endereço de carregamento dessas DLLs. Como são bibliotecas dinâmicas, o binário não mapeia um endereço fixo, então você pode mudar elas de lugar. E foi assim que eu consegui subir os programas Java que eu precisava. Quem já trabalhou com servidores 32-bits e programas realmente pesados de Java, com certeza esbarrou com isso, é um problema sabido desde o ano 2000 pelo menos. E pense que em 2003 o site StackOverflow ainda não existia.</p>
<p>Aceite este fato: a memória total do seu sistema não está à disposição do seu processo. Felizmente em máquinas 64-bits de hoje, você tem muito mais que 2 GB de endereços virtuais disponíveis, então pelo menos esse problema já não afeta mais. Tanto que mesmo na época uma das soluções que a SAP falava era: migre pra máquinas 64-bits. Espero que em 2019 ninguém mais esteja usando máquinas 32-bits. O segundo fato que você precisa aceitar: memória fragmenta. A razão é simples. Digamos que sua memória total seja de 10 kb, dividida em chunks ou pedaços de 1 kb. Você começa alocando 3 variáveis de 1 kb cada. Digamos que o alocador aloque os primeiros 3 chunks. Agora digamos que você resolva liberar a segunda variável. Você ficou com 1 kb ocupado, 1 kb livre, e o terceiro kb ocupado.</p>
<p>Agora seu programa resolve que precisa de uma variável de 2 kb. Como o espaço vago do meio não cabe os 2 kb, e repetindo, grave essa regra: o alocador sempre precisa alocar pedaços contíguos, com endereços sequenciais. Então ele só vai ter espaço depois do terceiro chunk. Agora você ficou com um espaço fragmentado. Pense numa escala maior, com milhares de alocacões e desalocações, sua memória vai ficando com buracos no meio do caminho e toda vez que você precisa alocar mais memória do que cabe nesses buracos ele precisa ir lá pra frente. Então um programa que internamente ele enxerga que está alocando só 4 KB na verdade está potencialmente bloqueando 5 KB nesse momento. Fragmentação é uma das coisas que alocador de memória quer evitar.</p>
<p>Quem faz esse trabalho é o alocador de memória. No caso do Linux, mais especificamente da biblioteca glibc, você tem o famoso “malloc”. Eu expliquei nos episódios anteriores que no Linux você pode escolher entre diferentes schedulers de threads como o CFS que hoje é o padrão ou outros como o BFS dependendo de pra que vai usar a máquina. Alocador de memória também, você pode escolher.</p>
<p>O malloc do glibc na verdade é o ptmalloc2 que por sua vez é baseado no dlmalloc, DL porque quem criou essa versão foi o professor Doug Lea. Um alocador de memória é um conjunto de algoritmos e estruturas para manter metadados dessa memória. Um alocador moderno tem vários de problemas importantes pra resolver. Primeiro de tudo, alocar memória precisa ser rápido, estamos falando de frações de milissegundos, ou seja, nanossegundos, porque é uma operação que acontece o tempo todo. Um ptmalloc2 leva uns 300 nanossegundos por operação. Mas não adianta ser rápido se ele “vaza” memória ou seja, usa mais memória do que o necessário, em parte porque não foi inteligente e deixou a memória fragmentar demais. Existem várias syscalls mas em C você normalmente chama pelas funções malloc, passando um tamanho que precisa alocar, e a função free que sinaliza ao sistema operacional que esse endereço pode ser liberado quando puder.</p>
<p>Quando você chama a função malloc, o único parâmetro que passa é quanto de memória quer alocar, e ele devolve como retorno o endereço que ele achou onde cabe um pedaço de dados do tamanho que você pediu. Se você tiver pouca RAM e vários processos fragmentando memória, você vai notar que teoricamente era pra caber mais programas na RAM mas por alguma razão falta memória. Um programa mal escrito pode estar alocando mais memória do que deveria, e não devolvendo. E mesmo devolvendo o sistema ainda pode não ter colocado essa memória à disposição.</p>
<p>Pra piorar, eu já expliquei sobre concorrência e paralelismo e todos os problemas que você enfrenta ao lidar com threads reais, lembra? Existe um outro problema: e se várias threads pedem ao alocador pra reservar memória ao mesmo tempo? Acabamos de criar uma contenção, ou seja, um gargalo. Se o alocador não suportar threads corretamente, o que acontece é um lock do alocador e cada thread tendo que esperar o anterior receber seu pedaço de memória. Na verdade o tal ptmalloc que eu falei é um fork do antigo dlmalloc justamente pra suportar threads.</p>
<p>Só de entender o problema de fragmentação e agora o problema de threads, você deveria começar a entender que fazer uma chamada em malloc não é um troço simples. Pra começar a forma de suportar threads é dividir o índice de endereços disponíveis no que o malloc chama de Arenas. Num Linux 32-bits o padrão é 2 vezes o número de cores. Então num computador quad-core com hyper threading você pode ter até 8 threads e por isso até 16 arenas. De forma simplista se todas as threads pudessem ter qualquer endereço do espaço de 32-bits, o alocador ia precisar criar um lock global e as threads perderiam totalmente sua performance e ficaram mais tempo em fila esperando o alocador do que realmente trabalhando em paralelo. Mas se você tem 4 cores e disser que do endereço 0000 0000 a 3FFF FFFF é da thread 1, do endereço 4000 0000 até 7FFF FFFF é da thread 2 e assim por diante, agora elas não compartilham endereços e você não precisa de um lock. Lembra que eu dei a dica já que a forma de você não ter que lidar com mutexes e locks em concorrência é não compartilhar nada entre as threads?</p>
<p>Agora as Arenas ainda são sub-divididas em Heaps ou montes. Seu programa quando executa pode armazenar dados na stack ou pilha ou no heap. A pilha é literalmente a pilha de execução, seu programa carrega as instruções na stack e pequenas quantidades de dados cabem no stack, se usar demais ou de forma errada vai ter o famoso Stack overflow. Se a memória fosse uma página de papel, pense na stack crescendo de cima pra baixo e o Heap começando de baixo pra cima. Não é exato mas é uma forma de visualizar. Na prática seu programa vai armazenar a maior parte dos dados que precisa pra trabalhar no Heap e na stack podem ficar coisas como valores constantes ou coisas assim.</p>
<p>Uma heap é ainda sub-dividido em múltiplos chunks. Podemos ter chunks muito pequenos de até 80 bytes, chunks de até 512 bytes e chunks grandes de mais de 512 bytes, que é como o ptmalloc organiza. Podemos agrupar os chunks menores em caixas ou bins, no caso os de até 80 bytes no que se chama Fast Bin, os de até 512 bytes num Small bin e os maiores que 512 bytes num Large bin. Por que isso? Pra minimizar fragmentação. Você fragmenta a memória mais rápido se tentar misturar pedaços grandes no meio de pedaços pequenos. É melhor agrupar pedaços de tamanhos similares mais próximos, porque se você desaloca um chunk de 16 bytes no Fast Bin, quando precisar alocar 16 bytes de novo, é mais fácil procurar direto no Fast bin. E se tentar alocar alguma coisa de 1 MB é melhor ir no Large Bin. E no começo você aloca poucos bins, e vai alocando mais à medida que precisar.</p>
<p>Lembre que toda essa organização ainda não está na prática “usando” a memória ainda. Está só criando estruturas que representam caixas de endereços. Daí a alocador vai pegar desses bins, dependendo de que thread está pedindo e que tamanho de memória pediu. Se você tem boa intuição está entendendo que seu processo sempre vai usar um pouco mais de memória do que ele realmente precisa por causa dessa organização e estruturas de suporte que o alocador precisa. Quanto mais tempo um processo fica vivo trabalhando, mais essas atividades de alocar e desalocar memória podem is expandindo, crescendo o heap e causando leaks se o programa for mal escrito, causando fragmentação demais.</p>
<p>Mas o pior não é isso, eu disse que Arenas são divisões para evitar contenção de lock de threads. E se sua aplicação inicialmente carregar 300 MB de estruturas na primeira arena. Mas agora numa segunda fase outra thread precisa trabalhar essas estruturas mas ela vai usar outra arena? Os 300 Mb vão ser copiados de uma arena pra outra porque o ptmalloc não suporta mover memória entre arenas. De repente seu programa inicia usando o dobro do que ele realmente precisa. Esse é um problema real relatado por engenheiros do Google e por causa disso eles criaram um outro alocador de memória, chamado de tcmalloc, que justamente permite cachear dados entre threads e por isso se chama Thread-Caching malloc ou tcmalloc.</p>
<p>Se algum dia esbarrar num tutorial que fala de tcmalloc, lembre que ele é do Google. Ele é mais rápido que o ptmalloc2 do glibc, chegando a ser até 6 vezes mais rápido e usando estruturas de dados mais eficientes, então também usando bem menos memória no geral. E como o tcmalloc implementa as mesmas funções POSIX como o malloc(), free(), calloc(), realloc() e outros. O Google mesmo diz que ele não é necessariamente 100% compatível e alguns programas podem ter problemas, mas na grande maioria, você consegue carregar o tcmalloc e seus programas devem funcionar mais rápido, usando menos memória e, se for um programa que fica de pé por dias seguidos como um servidor de aplicação, ele também deve tender a alocar menos memória no geral.</p>
<p>Tcmalloc foi criado mais de 10 anos atrás, e na mesma época, com o Firefox 3 o alocador de memória do Windows da época fragmentava demais, ou seja, o navegador tendia a crescer no uso de memória como eu já expliquei que acontece, numa ordem de pelo menos 20% ou mais de desperdício. Se o processo usar 1 GB ele vai desperdiçar 200 Mb só em fragmentação. Nessa época um engenheiro chamado Jason Evans queria resolver o problema de gerenciamento de memória de uma linguagem experimental que ele estava criando e pra isso fez um novo alocador. Esse alocador passou a ser usado no Firefox e foi um dos motivos de porque Firefox no Windows conseguia desperdiçar menos memória. Com o tempo esse alocador foi adotado pelo Facebook porque seus servidores também estavam desperdiçando muita memória. Esse alocador foi devidamente nomeado de jemalloc por causa de Jason Evans. Com o tempo o jemalloc se tornou mais rápido, mais robusto e confiável e desperdiçando ainda menos memória que o tcmalloc do Google. Inclusive acho que ele é mais compatível com o ptmalloc2 e você poderia trocar o alocador do seu Linux por jemalloc e tudo vai funcionar melhor.</p>
<p>De curiosidade, a linguagem Rust, assim como o Firefox, são da mesma fundação Mozilla e por isso o Rust desde o começo sempre usou o jemalloc, que é um dos motivos de porque ele é eficiente com memória. A grande maioria das outras linguagens ainda usam o ptmalloc2 do glibc. Linguagens como Python, Ruby também se beneficiam se você fizer o interpretador usar o jemalloc. Vou deixar nas descrições abaixo como fazer isso no Ruby.</p>
<p>Agora, isso vale pra linguagens que ativamente usam o alocador do sistema operacional. C, ou C++, ou Rust que já falei, todos que compilam pra binários nativos vão usar o ptmalloc2 ou o jemalloc hoje em dia. Pra variar, o Go não usa nenhum que já existia. Um ptmalloc2 usa funções de mais baixo nível ainda do sistema operacional como mmap, madvise, munmap, sbrk. O Go reinventa a roda e implementa seu próprio alocador meio inspirado, claro, no tcmalloc do Google. O Go também divide a memória em várias arenas. Esse conjunto de arenas é o que ele chama de Heap. Ele organiza as arenas em páginas de 8kb. Ele também divide esses blocos em bins pra tamanhos diferentes de chunks mas no caso do Go os nomes são diferentes ele tem um Tiny pra objetos menores que 16 bytes, um Small pra objetos menores que 32 kb e um Large pra coisa maiores que 32 kbytes. Esse valores dependem da versão do Go, então não decorem isso, só lembrem que o alocador agrupa blocos de tamanhos similares, como o ptmalloc2 já fazia também. E que é uma das estratégias pra diminuir a fragmentação.</p>
<p>Escovamos bits demais por hoje, vou ter que dividir esse episódio em duas partes de novo. Neste episódio tivemos mais noção de como o sistema operacional realmente organiza a memória, as diferenças de arquiteturas 32-bits ou 64-bits, e os diferentes tipos de alocadores. E como um bônus de termos já discutido sobre threads nos últimos episódios, vimos agora como eles afetam o gerenciamento de memória também. Na semana que vem vamos falar sobre os famosos garbage collectors, que muita gente ainda acha que é mágica, mas na prática não tem idéia de como funcionam.</p>
]]></content:encoded><category>performance</category><category>ciencia-da-computacao</category><category>programacao</category><category>akitando</category></item></channel></rss>