3 pontos por GN⁺ 2023-07-15 | 3 comentários | Compartilhar no WhatsApp
  • Para compilar mais software C existente com a Cosmopolitan Libc, foi preciso ir além de enganar o sistema de build: um patch de cerca de 2.000 linhas no GCC teve de fazer o compilador transformar automaticamente o código problemático
  • O obstáculo era que valores do sistema como SIGTERM e EINVAL não eram constantes em tempo de compilação, esbarrando nas regras da linguagem C para rótulos case em switch e inicialização de static/const struct
  • Foram tentados sed, expressões regulares em Python, plugins do GCC e até interceptação de macros do pré-processador, mas os erros de rótulos case ocorriam durante o parsing, dificultando bloqueá-los apenas com plugins
  • O patch final é ativado pela flag -fportcosmo; pouco antes de o GCC emitir o erro, ele transforma switch em if/goto e complementa inicializações de estruturas preenchendo valores em tempo de execução
  • Ainda há limitações, como estruturas anônimas, enum, alguns const int e índices de array dentro de expressões de inicialização, mas abriu-se um caminho para compilar softwares como bash, curl, git, ninja e gcc sem alterar o código-fonte

Objetivo do patch no GCC

  • Foi escrito um patch de cerca de 2.000 linhas no gcc para facilitar a criação de Actually Portable Executables com a Cosmopolitan Libc
  • Após aplicar o patch, é possível compilar os seguintes softwares com sistemas de build ./configure ou cmake
    • bash
    • curl
    • git
    • ninja
    • o próprio gcc
  • Os executáveis gerados devem poder rodar também em Linux, FreeBSD, MacOS, OpenBSD, NetBSD e Windows
    • Há a ressalva de que a execução no Windows não foi testada diretamente
  • Os binários gerados pelo GitHub Actions podem ser baixados em superconfigure releases

O ponto de bloqueio no porte para a Cosmopolitan Libc

  • Depois que a Cosmopolitan Libc chamou atenção em 2021 com o servidor web redbean, uma questão importante passou a ser quão bem softwares C existentes funcionam sobre essa libc
  • Ao portar runtimes de linguagens como Lua, Wren, Janet e quickjs, o mesmo padrão se repetia
  • Usar constantes do sistema como SIGTERM e EINVAL em rótulos case de um switch conflita com as regras de compilação de C
  • O padrão C exige que rótulos case sejam constantes em tempo de compilação
    • Na Cosmopolitan Libc, macros do pré-processador C relacionadas a interfaces do sistema precisavam ser simbólicas
    • Como resultado, código comum como switch(errno) { case EINVAL: ... } podia não compilar

Transformando switch em if e goto

  • Se rótulos como case EINVAL e case ENOSYS dentro de switch(errno) não forem constantes em tempo de compilação, o GCC emite o erro case label does not reduce to an integer constant
  • O patch transforma automaticamente a instrução switch na seguinte estrutura
    • Adiciona uma ramificação no formato if (errno == EINVAL) goto caselabel_EINVAL; para cada valor de case
    • Adiciona um goto que salta para o rótulo default
    • O bloco case original é reestruturado com rótulos e goto endofthis_switch
  • Essa abordagem não é a mais elegante, mas conseguiu lidar com os casos vistos em várias bases de código
  • Como era preciso lidar até com switch misturado com ifdef e fallthrough, a transformação baseada em goto era a opção simples

Inicialização de struct sofre a mesma restrição

  • O problema de constantes em tempo de compilação também aparece na inicialização de static ou const struct
  • Se EINVAL não for uma constante em tempo de compilação, algumas das inicializações abaixo não são válidas em C
    • Inicialização de uma struct local comum dentro de uma função é possível
    • const struct
    • static struct
    • const struct global
    • static struct global
  • Um caso real é o módulo faulthandler do CPython
  • A solução é inicializar a estrutura com valores fictícios e depois adicionar código para preencher os valores corretos antes do uso

Limites de substituição de texto e plugins do GCC

  • No início, tentou-se automatizar a transformação com um script shell usando sed; depois, tentou-se lidar até com fallthrough por meio de um script Python e expressões regulares
  • Mas, por causa do pré-processador C e de ifdef, a substituição de texto tinha dificuldade para funcionar de forma completa
    • Isso porque é difícil saber antecipadamente quais ifdef estarão ativos na compilação
  • A tentativa seguinte foi usar transformação de AST por meio de um plugin do GCC
    • Plugins do GCC podem ser carregados com -fplugin=your-plugin.so
    • É possível acessar a AST em eventos como PLUGIN_PRE_GENERICIZE, PLUGIN_FINISH_PARSE_FUNCTION e PLUGIN_FINISH_DECL
    • Com debug_tree, é possível imprimir a AST; com walk_tree_without_duplicates, é possível percorrê-la
  • O problema era que a validação de rótulos case ocorria durante o parsing do GCC
    • O plugin acessa a AST depois que o parsing termina
    • Um rótulo inválido como case SIGTERM: não permanece na AST
    • Como o GCC já havia emitido erro, era difícil corrigi-lo no plugin

Experimento de interceptação de macros do pré-processador

  • Como era difícil evitar erros antes do parsing apenas com o plugin, tentou-se um método que interagia com o pré-processador C
  • Pela estrutura cpp_reader dos headers de plugin do GCC, era possível registrar callbacks quando uma macro era definida com #define, removida com #undef ou usada
  • Usando a estrutura das macros de valores do sistema da Cosmopolitan Libc, eram inseridas constantes temporárias
    • Ex.: extern const int SIGTERM;
    • Ex.: #define SIGTERM SYMBOLIC(SIGTERM)
    • Para o plugin, mantinha-se um valor temporário como static const int __tmpcosmo_SIGTERM = ...
  • O plugin interceptava o uso de SYMBOLIC(SIGTERM), substituía-o pelo valor temporário e, depois, na etapa PLUGIN_PRE_GENERICIZE, alterava novamente a AST para o VAR_DECL correto
  • Esse hack de macros conseguiu contornar os erros de inicialização em switch e struct
  • Com essa abordagem, tornou-se possível compilar pelo menos o CPython 3.11

Por que corrigir o próprio GCC em vez de usar um plugin

  • Depois de uma discussão com Justine Tunney, ao confirmar que o problema podia ser resolvido, foi sugerida uma abordagem mais simples: modificar diretamente a base de código do GCC
  • O plugin e o hack de macros envolviam muito trabalho desnecessário, e o tratamento de casos extremos e crashes era complexo
  • Por exemplo, mesmo em uma situação em que bastaria transformar case ENOSYS, por causa da substituição de macros o plugin também precisava lidar com usos normais como ENOSYS + 1 ou printf("ENOSYS = %d", ENOSYS)
  • Na abordagem com patch no GCC, a funcionalidade é ativada pela flag -fportcosmo, em vez de um -fplugin separado
  • Pouco antes de o GCC emitir o erro case is not constant, ele verifica se flag_portcosmo está ativo e, se estiver, realiza as substituições necessárias
  • O código de transformação de AST do plugin existente foi movido para dentro do GCC, para ser executado antes da chamada de outros callbacks de plugin
  • Com essa mudança, foi possível remover o código de hack relacionado a macros

Builds reais e expansão do patch

  • Em 5 de junho de 2023, o GCC com patch passava em todos os casos de teste do plugin anterior, e um novo binário foi adicionado ao monorepo da Cosmopolitan Libc
  • À medida que mais código era compilado, correções e melhorias continuaram
  • O build de lua ficou mais simples, e, ao encontrar onde o erro de case constant ocorria no g++, também se tornou possível compilar ninja
  • Ao tentar anexar ncurses ao Python 3.11, um novo problema foi encontrado
    • Em ncurses, havia código com uma expressão binária em valores de inicialização de estrutura, como DATA2(PARODD | PARENB, PARODD)
    • O patch existente tratava inicializações com constantes simples, mas não expressões como PARODD | PARENB
  • Depois, o patch foi expandido para permitir que rótulos case e elementos de inicialização de estruturas em C pudessem ser expressões arbitrárias
    • Pelo padrão C, rótulos case ainda precisam ser constantes em tempo de compilação
    • O compilador com patch emite um aviso para esse uso, mas continua a compilação
    • C++ foi excluído da substituição de expressões arbitrárias porque ela não combinava com o tratamento de constexpr do g++11
  • Após essa mudança, ncurses passou a compilar sem problemas

Restrições restantes e conclusão prática

  • O patch não é perfeito
    • Não consegue tratar algumas estruturas anônimas
    • Não consegue tratar alguns casos de enum e const int
    • Não consegue tratar casos em que SIGTERM é usado como índice de array dentro de uma expressão de inicialização
    • Casos estranhos de inicialização static ou situações em que SIGTERM é colocado em um static const int8_t ainda podem causar crashes raros do compilador
  • Ainda assim, muitos contraexemplos óbvios foram removidos, e vários softwares populares passam a compilar de forma fluida
  • Ambientes de teste mais rigorosos podem revelar pontos para novas melhorias
  • Se quiser, também é possível corrigir manualmente instruções switch e expressões de inicialização de struct, mas, em muitos casos, o compilador consegue fazer isso automaticamente
  • Se um software C puder ser compilado estaticamente, especialmente também com musl, há chance de que ele também possa ser compilado com a Cosmopolitan Libc

3 comentários

 
ryuheechul 2023-07-15

Então agora o neo está adotando um formato em que as mensagens ficam envoltas em blocos de código? É um pouco chato ter que fazer rolagem horizontal, e a cor do texto em tons de vermelho também deixa um pouco a desejar...

 
xguru 2023-07-16

Ah, isso ser envolvido em um bloco de código é um bug T_T

 
GN⁺ 2023-07-15
Opiniões no Hacker News
  • Eu escrevi este texto, mas o título deveria ser “Patching GCC to build Actually Portable Executables”, porque ele se refere à Cosmopolitan Libc e ao formato Actually Portable Executable do jart.
    Com o meu patch para o GCC, agora é possível compilar softwares como vim, emacs, ninja, bash, git e gcc com a Cosmopolitan Libc, usando os sistemas de build habituais no estilo autotools/cmake. Os executáveis gerados devem conseguir rodar em Linux, FreeBSD, MacOS, OpenBSD, NetBSD e Windows. Porém, ainda não testei no Windows.
    A lista de softwares que compilei usando essa técnica está aqui: https://github.com/ahgamut/superconfigure
    O script superconfigure é apenas um wrapper em torno do script configure comum usado para compilar software, adicionando flags como --enable-static.
    Se quiser compilar o GCC com a Cosmopolitan Libc, você pode usar este repositório: https://github.com/ahgamut/musl-cross-make/tree/gccbuild

    • Fico curioso para saber se há alguma chance de esse patch entrar no upstream do GCC. Executáveis APE e a cosmopolitan libc são tecnologias muito legais, então seria ótimo se ficassem mais fáceis de usar.
    • Legal. Fico curioso se você acha que podem surgir problemas ao usar isso por trás de linguagens transpiladas, como Nim.
    • Fico curioso se você também tentou compilar o busybox.
    • Recentemente vi você ser banido no lobste.rs sob a alegação de ser um sockpuppet do jart, mas só agora percebi que você é mesmo outra pessoa.
      Espero que receba um tratamento melhor no HN.
    • O simples fato de compilar não é uma conquista tão grande; o mais interessante é se funciona de fato.
      Por exemplo, fico curioso se houve testes suficientes para demonstrar que não há regressões em comparação com a versão glibc.
  • Há uma issue relacionada a isso no repositório do Go: https://github.com/golang/go/issues/51900

  • No geral, é bem legal, mas fico curioso sobre a fundamentação teórica. Se você está criando um novo alvo de sistema operacional, logicamente parece fazer sentido definir novas constantes.
    Em vez de repassar as constantes de cada sistema operacional subjacente, bastaria converter as constantes de cada SO para que fossem iguais em todos os lugares. Assim, código que usa constantes como índices de array ou em instruções case também funcionaria corretamente.
    Isso pode ter mais overhead do que a abordagem descrita no texto, mas parece muito mais fácil e mais simples de usar para quem quer compilar novos programas para a Cosmopolitan libc.

    • Criar novas constantes torna a compilação muito mais fácil, mas, pessoalmente, acho que o overhead de converter em tempo de execução entre as constantes existentes e as novas seria grande demais.
      A lista de constantes está aqui: https://github.com/jart/cosmopolitan/blob/master/libc/sysv/c...
      Cada vez que uma dessas constantes fosse usada, seria preciso incluir muitas constantes no binário em uma grande tabela de consulta, e toda vez que o programa precisasse verificá-las teria que passar por essa tabela. Talvez não fosse extremamente lento, mas acredito que certamente seria perceptível.
      O objetivo era facilitar o porte sem alterar muito o código-fonte da libc ou do software que se quer portar, e produzir binários com desempenho semelhante ou melhor. Dentro dessas restrições, este patch do GCC pareceu ser a melhor forma de simplificar o processo.
      Se eu encontrar um número suficiente de codebases que usam SIGHUP para inicializar índices de arrays, penso em testar a abordagem sugerida e medir o trade-off. Ou você mesmo pode tentar e me contar se um conjunto separado de constantes é melhor.
  • Se “você passou bastante tempo eliminando contraexemplos óbvios, e muitos softwares populares compilam sem problemas”, seria útil publicar uma lista de softwares que compilam com sucesso. Talvez ela já exista.

    • É possível compilar softwares como vim, emacs, ninja, bash, git e gcc. A lista que compilei usando essa técnica está aqui: https://github.com/ahgamut/superconfigure
      O script superconfigure é apenas um wrapper em torno do script configure comum usado para compilar software, adicionando flags como --enable-static.
  • Pergunta-se se há algum objetivo prático para o APE. Existe algum lugar que realmente distribua um único executável que precise rodar em vários sistemas operacionais?

    • O Logfile Navigator (https://lnav.org) usa um executável APE como agente que se comunica com hosts remotos
      O próprio lnav não é compilado como APE, mas o agente embutido nele é. Quando o usuário quer ler os logs daquele host, esse agente é enviado remotamente
      Assim, não é necessário ter uma etapa extra para detectar separadamente o tipo de sistema operacional ou incluir várias versões do executável. Há um texto curto relacionado aqui: https://lnav.org/2021/05/03/tailing-remote-files.html
    • Vêm à mente programadores de Java/Clojure/Kotlin, programadores de .Net, JS, Python, Ruby, Erlang e de todas as linguagens baseadas em máquina virtual
      Programas compilados estaticamente estão na base de tudo, mas, no nível de aplicação, são minoria
      O APE levanta a grande pergunta: “se uma linguagem AOT, de compilação antecipada, de baixo nível como C pudesse ser compilada uma vez e executada em qualquer lugar, quem precisaria de uma máquina virtual?”
    • Por exemplo, em lugares como repositórios Maven, onde é trabalhoso manter vários artefatos para a mesma combinação id/version, isso poderia tornar a distribuição de executáveis um pouco mais fácil. O PyPI provavelmente tem um problema parecido
      Além disso, cross-compilation é doloroso. Projetos open source pequenos que não têm uma build farm com vários BSDs muitas vezes simplesmente não distribuem binários para essas plataformas
      Até o kubectl tem binários para Linux, mas não para BSD — a menos que se conte o OSX como BSD. Então isso parece uma vitória fácil para esse tipo de projeto
    • Pretende-se lançar futuros side projects dessa forma. Mesmo para SPAs apenas de cliente, compila-se um executável Redbean. A ideia de que essas coisas simplesmente funcionem em vários sistemas é impressionante
    • Há um compilador de shaders para o qual ajudaria ter um formato de executável independente de plataforma e executável imediatamente
      Ele é grande demais para ser compilado na hora na máquina do usuário. Inclui várias dependências C++ grandes de terceiros e, dependendo do hardware, a compilação leva de 2 a 20 minutos
      Precisa rodar em macOS x86-64+ARM, Linux x86-64 e Windows x86-64. Seria bom se também rodasse em ARM Linux
      No momento, WASM está sendo avaliado, mas isso exige um runtime WASI instalado
  • Para quem estiver confuso: “Actually Portable Executables” https://justine.lol/ape.html

    • Foi adicionado um comentário mencionando isso, mas ainda não mudou. Acredito que vai mudar em breve. Será que posso chamar o @dang aqui?
  • Trocar switch por if é bastante tosco. Acho que daria para corrigir mapeando para um conjunto de constantes neutras de plataforma e desreferenciando os símbolos de runtime por meio de uma função
    Por exemplo, usando algo como switch(CosmoMappErrno(EINVAL)) e case COSMO_EINVAL:, não seria necessário transformar o código em um monte de goto

    • Mas isso não ajuda a compilar código não modificado. Não é preciso se preocupar com gotos gerados internamente pelo compilador; compiladores já fazem isso por toda parte
      Claro, seria diferente se a ideia fosse o compilador reconhecer esses switches e, em vez de reescrevê-los como uma árvore de if, trocar os labels por constantes especiais específicas do cosmo para cada valor e envolver o valor de entrada do switch em uma chamada de função que mapeie o valor da plataforma de runtime atual para essa constante específica do cosmo, deixando outros valores passarem sem alteração
      Isso poderia, na prática, ser uma transformação de compilador mais simples. Só que surgiria a complexidade de reconhecer qual dos vários conjuntos de constantes está sendo usado e aplicar a chamada de mapeamento adequada ao valor de entrada do switch. Também seria necessário criar esse mapeamento no lado da biblioteca, o que adiciona complexidade
      Como já existe uma implementação funcionando com outra forma de transformação, não sei se o autor teria vontade de implementar isso
      Por fim, esse método de mapeamento traz o risco de mapear errado e também detalhes complicados, como ter de inverter a entrada antes do mapeamento em código que usa valores negativos de errno
    • A abordagem de desreferenciar símbolos de runtime por meio de uma função parece interessante. Se você mostrar um exemplo funcionando, eu ficaria feliz em tentar
      O motivo de eu gostar da matriz if-else-goto é que ela se encaixou perfeitamente em outras partes do GCC. Olhando meu patch[1], dá para ver que foi preciso alterar muito pouco do código existente do GCC para adicionar esse recurso
      [1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
    • É preciso se preocupar até com a substituição acontecer automaticamente?
  • Não entendo por que causa tanta surpresa código que usa SIGxxx como índice. Números de sinal são bem definidos e, em geral, sabe-se que são contínuos e começam em 1

    • Antes de tentar o busybox, compilei várias bases de código como git, bash, gcc e curl, e não tinha visto esse tipo de código
      Além disso, esse erro apareceu junto com um erro do compilador no meu patch que levou bastante tempo para depurar, então ficou como uma surpresa marcante
      Fico curioso para saber se esse padrão de código é considerado comum. Também gostaria de saber em quais bases de código, além do busybox, ele aparece. Se houver muitos exemplos, talvez valha a pena dedicar tempo para atualizar o patch para lidar com esse padrão
  • Se errno é um problema tão grande assim, não sei que esperança há de fazer a biblioteca padrão C++ e std::error_code funcionarem
    Lá não há como usar macros mágicas

    • O problema parece ter sido justamente essa macro mágica. Normalmente macros se expandem para constantes, mas agora não é mais assim, então algumas coisas quebram
      std::error_code não é uma constante, então não deve ter o mesmo problema