3 pontos por GN⁺ 2024-04-09 | Ainda não há comentários. | Compartilhar no WhatsApp
  • Explorando o mundo de abstrações escondido por trás de um programa Hello World moderno

    • Este texto trata de um programa Hello World escrito em C. Entre as linguagens de alto nível em que não é preciso se preocupar com o que a linguagem faz antes de o programa realmente rodar em um interpretador/compilador/JIT, C é a de nível mais baixo.
    • A intenção original era escrever algo que qualquer pessoa com experiência em programação pudesse entender, mas ter pelo menos algum conhecimento de C ou assembly provavelmente ajuda.
  • Começando com o programa Hello World

    • Todo mundo provavelmente já está familiarizado com o programa Hello World. Em Python, talvez o primeiro programa que você escreveu tenha sido algo como print('Hello World!').
    • Neste texto, vamos olhar para um Hello World escrito na linguagem de programação C. Em C, não dá para executar um programa chamando um interpretador. Primeiro é preciso rodar um compilador para convertê-lo em código de máquina que o processador do computador possa executar diretamente.
  • Analisando nosso programa

    • Ao analisar o arquivo do programa compilado, dá para ver que ele é um executável ELF e destinado ao conjunto de instruções x86-64.
    • Um executável ELF é o equivalente, no Linux, a um arquivo .exe do Windows.
    • x86-64 é a arquitetura de CPU usada em PCs desde a introdução do IBM PC em 1981.
    • Esse arquivo contém código de máquina, a única linguagem que a CPU consegue entender.
  • Analisando o código assembly

    • Encontramos o entry point, o endereço inicial do programa, e analisamos o código assembly.
    • Assembly é uma representação legível por humanos do código de máquina.
    • Dá para ver o código de inicialização adicionado automaticamente pelo compilador, mais precisamente pelo linker, e notar que ele chama a função __libc_start_main.
    • Mas esse código não está definido no nosso programa; ele está em outro lugar.
  • Biblioteca padrão de C

    • A função __libc_start_main está definida em libc.so.6, a biblioteca padrão de C do nosso sistema.
    • A biblioteca padrão de C é um conjunto de rotinas e funções usado por quase todos os programas do computador.
    • A biblioteca C faz o trabalho de inicialização e chama a função main() que escrevemos. Quando main() retorna, ela encerra o programa com o código de saída fornecido.
  • Análise da função main()

    • Na função main(), é criado o stack frame, o endereço da string Hello World é configurado como argumento da chamada de função e, em seguida, puts() é chamada.
    • puts() aparece no lugar de printf() porque o compilador fez essa otimização. printf() é mais complexa, enquanto puts() apenas imprime uma string sem formatação.
  • A string Hello World

    • A string tem a forma "Hello World!" seguida por um terminador NULL.
    • Em C, não há informação de tamanho associada à string, então o terminador NULL marca o fim dela. Sem ele, o programa continuaria lendo memória não permitida e acabaria morrendo com um Segmentation Fault.
    • Por causa da otimização do compilador, a quebra de linha (\n) usada em printf() foi removida. puts() já adiciona uma quebra de linha depois de imprimir a string.
  • A função puts()

    • A função puts() por sua vez acaba chamando código dentro da biblioteca padrão.
    • Observando o código da glibc, é possível ver as chamadas _IO_puts -> _IO_new_file_xsputn, mas o código é complexo demais para explicar com facilidade.
    • No caso da musl libc, é um pouco mais simples. A sequência é puts -> fputs -> fwrite -> __fwritex -> __stdio_write -> syscall.
  • Chamada de sistema

    • Por maior que a biblioteca C seja, ela não consegue se comunicar diretamente com o hardware. Só o kernel pode fazer isso.
    • Portanto, a chamada de puts() termina pedindo ao sistema operacional que faça algo. Aqui, trata-se de escrever a string em um fluxo de saída.
    • A musl libc usa a chamada de sistema writev, que permite escrever vários buffers de uma vez.
    • A chamada de sistema acontece configurando os parâmetros em registradores e executando a instrução syscall. Então o controle passa para o kernel, que lê os parâmetros e executa a chamada de sistema.
  • Kernel

    • O kernel Linux precisa executar a ação solicitada pela chamada de sistema. A chamada write instrui o kernel a escrever em um arquivo ou fluxo aberto no sistema de arquivos.
    • write recebe três parâmetros: o descritor de arquivo para escrita, o buffer a ser escrito e a quantidade de bytes a gravar.
    • Onde exatamente será escrito depende da situação. No caso de um emulador de terminal, aparece como um terminal virtual (pty); em um login remoto, é encaminhado ao sshd; em um terminal físico, vai para um adaptador serial-USB. No caso de um console em frame buffer, o kernel renderiza o texto e o mostra na tela.
  • Conclusão

    • Os sistemas de software modernos funcionam de forma extremamente complexa e sofisticada sobre o hardware, então tentar compreender completamente como o computador realiza uma pequena tarefa chega a ser inútil.
    • Para explicar tudo, foi inevitável omitir muita coisa.
    • Enviar a mensagem Hello World é apenas uma entre inúmeras chamadas de sistema e programas em execução neste computador neste momento.

Opinião do GN⁺

  • É um texto que mostra como cada camada de um sistema computacional esconde a complexidade da camada inferior por meio de abstrações, permitindo que desenvolvedores criem aplicações com mais conveniência.
  • Ao mesmo tempo, ele faz perceber quanta coisa acontece por baixo para que uma única linha da aplicação seja executada, e também por que depurar é tão difícil.
  • Acho que todo programador deveria conhecer bem ao menos o sistema que está abaixo da linguagem que usa no dia a dia. Não é preciso saber tudo, mas é importante entender como as partes abstraídas realmente funcionam.
  • Mesmo usando linguagens de alto nível, estudar conceitos de programação de sistemas como estrutura de memória, stack e heap, e chamadas de sistema pode ajudar muito em depuração e otimização de desempenho.
  • Desenvolvedores de aplicações quase nunca precisarão mexer diretamente no compilador ou na biblioteca C, mas entender de que forma o programa que escreveram acaba usando o sistema é, na minha opinião, essencial para se tornar um bom programador.

Ainda não há comentários.

Ainda não há comentários.