1 pontos por GN⁺ 17 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • Código que segue apenas o padrão ISO C é raro, e codebases C reais dependem de extensões não padronizadas para adicionar funcionalidades e contornar lacunas específicas de compiladores e bibliotecas
  • Um compilador C útil precisa ao menos processar cabeçalhos do sistema como <stdio.h>, mas a glibc cria barreiras com extensões GNU e suposições como __attribute__((packed)) e #include_next
  • A lógica de byteswapping da SDL pode escolher inline assembly quando há macros de ISA, o que pode acabar exigindo extensões no estilo GCC até de compiladores que não são GCC nem clang
  • O tratamento de extern inline em OpenBSD e Gnulib torna a compatibilidade da semântica de inline complicada por causa das diferenças entre C99 e GCC, ramificações por plataforma e condições como _FORTIFY_SOURCE
  • Compiladores C pequenos precisam escolher entre patches upstream, patches downstream, obter guards dedicados ou imitar compatibilidade com GCC, e a expansão de macros de teste de recursos parece um caminho melhor

A primeira barreira criada pelos headers da glibc

  • Para ser um compilador C útil, é preciso conseguir preprocessar e fazer parsing dos headers da biblioteca C do sistema, e se não conseguir processar <stdio.h>, fica difícil até passar por um hello world
  • Em ambientes GNU/Linux, essa barreira leva à glibc
  • A glibc verifica macros predefinidas pelo compilador em sys/cdefs.h, incluído indiretamente por quase todos os headers da libc, para determinar quais extensões são suportadas
  • Extensões não suportadas são tratadas removendo definições relacionadas, mas essa lógica de compatibilidade também pode quebrar na prática
  • struct epoll_event e __attribute__((packed))

    • A struct epoll_event em sys/epoll.h no Linux é uma packed struct que usa GNU __attribute__((packed))
    • Esse atributo altera o layout da struct em 64 bits, então ignorá-lo quebra a ABI
    • Não basta que o compilador implemente __attribute__((packed))
    • Em sys/cdefs.h, há código que define __attribute__(xyz) como uma macro vazia se não for GCC, clang ou tcc
    • Como resultado, outros compiladores podem ter esse atributo removido nos headers da glibc mesmo que suportem packed
    • Também é possível argumentar que, como o header de epoll é específico de Linux, é difícil aplicar diretamente os critérios de portabilidade do padrão C
  • limits.h e #include_next

    • Alguns headers C, como stddef.h, stdint.h, limits.h e float.h, também são necessários em implementações freestanding, então precisam ser fornecidos pelo compilador
    • O POSIX exige que limits.h defina, além das constantes padrão de C, constantes específicas do POSIX, então é necessário um limits.h da plataforma sobre o limits.h do compilador
    • O <limits.h> da glibc define diretamente valores ANSI limits.h quando não está em GNU C e, em ambiente GCC, puxa o header do compilador com #include_next <limits.h>
    • Essa estrutura assume que o limits.h builtin específico do GCC define certas macros e também depende da extensão #include_next
    • O clang também precisa contornar essa estrutura

Detecção de recursos na SDL e o problema de inline assembly

  • As funções de byteswapping em SDL_endian.h usam builtin do compilador ou inline assembly quando possível, e no último caso recorrem a uma implementação comum com operações de bits
  • A lógica de detecção funciona aproximadamente nesta ordem
    • Se for GCC ou clang e houver __has_builtin(__builtin_bswapX), usa builtin
    • Se for MSVC 8.0 ou superior, usa #pragma intrinsic do MSVC
    • Se macros específicas de ISA como __x86_64__ estiverem definidas, usa inline assembly
    • Caso contrário, usa a implementação comum com operações de bits
  • Se um compilador que não é GCC nem clang definir macros predefinidas específicas de ISA por um motivo razoável, essa ordem vira um problema
  • Mesmo que esse compilador forneça builtin de bswap e o operador especial __has_builtin, a lógica pode acabar tentando usar inline assembly no estilo GCC
  • Na prática, a estrutura passa a presumir que um compilador desconhecido também suporta inline assembly no estilo GCC

OpenBSD libc e a confusão de extern inline

  • Alguns headers do OpenBSD incluem definições de funções inline que o compilador pode usar opcionalmente durante otimização
  • Essas funções são definidas com a macro __only_inline, e se o compilador não fizer inline de fato, elas precisam ser substituídas por símbolos externos
  • Ou seja, é necessária uma função inline com ligação externa
  • Diferenças entre inline em C99 e inline no GCC

    • inline está especificado no C99, mas o comportamento padrão entra em conflito com o comportamento não padronizado do GCC anterior ao C99
    • Definições inline dentro de headers precisam usar extern inline junto com o corpo da função e, nesse caso, não emitem a função exportada real
    • Na translation unit, é preciso declarar a função apenas com inline para exportar sua definição
    • O significado de inline também é diferente em C++ e em C
    • Essa diferença é tratada em detalhe no texto de Youtao Guo
  • __only_inline no OpenBSD

    • O OpenBSD depende da semântica de inline do GCC
    • Para encobrir diferenças entre versões do GCC, a macro __only_inline em sys/cdefs.h especifica explicitamente a antiga semântica gnu89 inline em GCCs recentes por meio de __attribute__
    • Em compiladores não GNU, __only_inline é definido com linkage static
    • Como resultado, a função pode ser declarada e definida com linkages conflitantes, quebrando a compilação
  • O contorno _ANSI_LIBRARY

    • O OpenBSD respeita a macro _ANSI_LIBRARY
    • Se essa macro for definida, o uso das definições problemáticas de __only_inline é totalmente omitido em headers padrão como signal.h
    • Você não obtém a versão otimizada, mas pelo menos o build funciona
  • O código de compatibilidade de extern inline no Gnulib

    • O código de compatibilidade de extern inline do Gnulib também aparece ao compilar Guile e nano
    • extern-inline.m4 contém uma lógica condicional complexa para lidar com implementações quebradas e estranhas desse caso extremo de C
    • As condições refletem diferenças de ambiente envolvendo Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C, _FORTIFY_SOURCE, __GNUC_STDC_INLINE__ e __GNUC_GNU_INLINE__

As suposições de clang no bionic do Android

  • O bionic é a libc do Android, e seus headers pressupõem clang de forma ainda mais forte que GCC
  • Os headers do bionic usam bastante extensões específicas do clang, como _Nonnull e _Null_unspecified, para nullability checks
  • Essas macros não são tão difíceis de neutralizar via #define na linha de comando
  • Ao usar um celular Android como ambiente nativo de desenvolvimento aarch64 via Termux, esse problema aparece nos headers do bionic
  • _Null_unspecified também é chamado de __BIONIC_COMPLICATED_NULLNESS, e a definição relacionada está em sys/cdefs.h` do bionic

As escolhas que compiladores C pequenos enfrentam

  • Código que segue apenas o padrão ISO C é raro na prática, e muitas codebases C dependem de comportamento não padronizado e de extensões da linguagem
  • Essa dependência surge não só por funcionalidades extras, mas também ao contornar bugs e lacunas diferentes entre compiladores e bibliotecas
  • Codebases que tentam suportar vários ambientes dependem de verificações e guards no preprocessador, mas essa abordagem quebra facilmente e é difícil de manter
  • Ao criar um compilador C como o antcc, esses problemas de compatibilidade aparecem repetidamente
  • Quando muitos projetos open source dependem de extensões e comportamentos não padronizados específicos de compilador mesmo para coisas não essenciais, a carga para compiladores alternativos aumenta
  • Ao mesmo tempo, é difícil exigir que todo desenvolvedor teste código C em vários compiladores, incluindo os pequenos e menos conhecidos
  • Portabilidade em C já é difícil por si só
  • Do ponto de vista de quem escreve compiladores, há quatro caminhos possíveis
    • Tentar corrigir a incompatibilidade com patches upstream
    • Ficar conhecido o suficiente para que desenvolvedores adicionem verificações #ifdef dedicadas e testes básicos por padrão
    • Resolver no downstream e distribuir patches ou patches separados
    • Fingir ser uma versão específica do GCC e implementar essas extensões
  • Patches upstream parecem uma batalha difícil de vencer, e patches downstream são o caminho mais fácil
  • Para suportar muitas codebases com o mínimo de confusão para usuários e desenvolvedores, imitar compatibilidade com GCC é realista, mas traz um grande custo de implementação
  • O clang define __GNUC__=4, __GNUC_MINOR__=2 e __GNUC_PATCHLEVEL__=1 para alegar compatibilidade com GCC 4.2.1
  • Hoje o clang é quase um alvo de suporte separado, mas foi necessário um grande esforço, com patches em ambos os projetos, só para viabilizar compilar o kernel Linux com clang

Macros GCC e o problema de correr atrás

  • Fingir ser GCC também traz problemas
  • Muitas codebases verificam apenas #ifdef __GNUC__ e podem usar extensões modernas do GCC sem checar a versão
  • Nesse caso, compiladores alternativos precisam continuar correndo atrás
  • Esse é um dos motivos pelos quais o clang, apesar de suportar extensões GNU mais novas que 4.2.1, não aumenta o valor da macro __GNUC__
  • O contexto relacionado aparece na discussão do LLVM sobre elevar a minor version de __GNUC__

Um caminho melhor e o estado atual

  • Idealmente, macros de teste de recursos deveriam ser usadas de forma mais ampla no lugar de guards por compilador e checagens de versão
  • Algumas macros úteis para isso são __has_builtin, __has_feature e __has_attribute
  • Abordagens com macros padrão, como __STDC_NO_VLA__, também poderiam ser mais comuns
  • No mundo *NIX atual, para o bem ou para o mal, o estado padrão é um quase duopólio GCC/clang
  • O desenvolvimento de compiladores C pequenos e independentes continua

1 comentários

 
Comentários no Lobste.rs
  • (autor do compilador kefir) O problema de __attribute__ em <sys/cdefs.h> está, pela minha experiência, entre os mais dolorosos. Ele quebra epoll, as estruturas packed comuns, construtores e visibilidade de símbolos, então acabei incluindo este header com monkey patch junto com o kefir
    Não é o ideal, mas provavelmente é a abordagem mais realista e, na prática, isso permitiu remover a maior parte dos patches customizados na suíte de testes externa
    Outro tipo de falha é código alternativo com bugs. Alguns projetos tentam detectar o compilador para se ajustar, mas como são pouco testados com compiladores alternativos, o código de fallback acaba cheio de bugs ou mal mantido. Do ponto de vista de quem escreve um compilador, isso é muito mais irritante do que falhar imediatamente com “compilador não suportado”. Por exemplo, porque você precisa depurar diretamente miscompilações estranhas, como incompatibilidades na largura de typedef inteiros entre o programa e bibliotecas pré-compiladas

    • Algo parecido acontece com terminais. Se você não configurar $TERM como xterm-256color e fingir ser um xterm, um monte de coisa quebra
      Realmente não faço ideia de como resolver isso. No fim, será que nosso projeto só precisa ficar difundido e famoso o bastante? Fácil!
    • A abordagem de header com monkey patch parece ser a mesma usada pelo slimcc, e parece um compromisso bem razoável
      Acho que também já passei algumas vezes por essas miscompilações estranhas causadas por fallbacks de detecção de compilador mal mantidos, e é realmente irritante
  • Como desenvolvo o cproc principalmente em linux-musl, eu não sabia que a glibc desativa __attribute__ para outros compiladores, mas a situação é de fato bem ruim. Os comentários dizem que não tem problema ignorar o uso de attributes, mas isso não leva em conta que a maioria do código de aplicação inclui sys/cdefs.h indiretamente e pode usar attributes que não devem ser ignorados
    Além de packed, aligned e constructor também são bastante usados
    Fico curioso se isso foi reportado em algum issue tracker. Parece que a maior parte do uso de attributes dentro de cdefs.h já é protegida por __glibc_has_attribute, então também fico me perguntando o que essa desativação geral de __attribute__ realmente pretende alcançar e se ela poderia ser removida
    Também é um problema quando headers da libc usam recursos para os quais o compilador não tem uma boa forma de indicar suporte. São recursos que não aparecem por mecanismos como __has_attribute ou __has_builtin; um exemplo que me vem à cabeça são os rótulos __asm__. O NetBSD os usa para renomear símbolos e emite #error se não houver __GNUC__ ou __PCC__. Mas, fora deixar tentar e falhar se não houver suporte, também não sei bem o que sugerir
    Também já tive problemas relacionados a __builtin_va_list. Há casos em que a libc, sem __GNUC__, define va_list como void *, ou até coloca definições conflitantes. Isso também não dá para testar com __has_builtin. __has_builtin(__builtin_va_arg) até pode ser um teste bom o bastante, mas não faço ideia de como convencer o macOS a corrigir isso

    • Fazendo uma busca rápida pelo uso de __attribute__ em /usr/include/sys e /usr/include/bits, encontrei muitos usos sem proteção. Em geral eram __format__, __aligned__ e __noreturn__, então isso também precisaria ser corrigido
      No geral, a glibc não parece tratar compatibilidade com compiladores que não sejam GCC como prioridade, então não sei qual seria a chance de aceitarem esse tipo de patch. No começo deste ano, depois de uma atualização do sistema, a glibc adicionou aos headers do Linux um uso não protegido de __SIZE_TYPE__, e isso fez meu compilador deixar de compilar alguns projetos. Eu reportei, mas ainda não foi corrigido, e no fim acabei adicionando macros predefinidas no estilo __X_TYPE__ para ficar compatível com o GCC
      Quanto ao problema dos rótulos __asm__, não me ocorre uma solução muito boa. Mas, se a renomeação de nomes em asm for realmente 100% necessária para o funcionamento, talvez seja melhor simplesmente tentar e deixar falhar do que fazer uma checagem de compilador
      __builtin_va_list é bem sério. Eu esperava que __has_builtin(__builtin_va_list) funcionasse, mas aparentemente não funciona