1 pontos por GN⁺ 2024-09-30 | 1 comentários | Compartilhar no WhatsApp
  • Em sistemas glibc de 32 bits, consultas da hora atual ou chamadas a stat() podem falhar após 2038, então o Gentoo precisa de um caminho seguro para migrar para time_t de 64 bits
  • O time64 da glibc deve ser usado junto com Large File Support (LFS) e, em ambientes de 32 bits, coexistem a ABI antiga, a ABI LFS e a ABI LFS+time64
  • Quando time_t aparece em APIs, estruturas ou argumentos de funções, a mudança na largura do tipo leva a uma quebra de ABI, criando mau funcionamento em tempo de execução e riscos de segurança ao misturar binários time32 e time64
  • Como distribuição baseada em código-fonte, o Gentoo pode ficar com um sistema parcialmente migrado durante a recompilação de @world, por causa de falhas ou dependências circulares
  • Após a correção de 2024-09-30, ficou claro que apenas mudar o libdir não basta, e uma marcação time64 envolvendo o carregador dinâmico e várias toolchains de linguagens continua sendo a principal restrição

O problema de 2038 e o escopo da migração para time64

  • Aplicações de 32 bits que usam time_t de 32 bits podem receber um erro -1 em vez da hora atual em 2038, ou podem não conseguir executar stat() em arquivos
  • A direção básica da transição é mudar time_t para um tipo de 64 bits
    • O musl já fez a transição
    • A glibc oferece suporte a isso como opção
    • Algumas distribuições, como o Debian, já fizeram a transição
  • Em uma distribuição baseada em código-fonte como o Gentoo, o próprio usuário recompila o sistema, então é preciso reduzir o tempo em que os pacotes permanecem em estados de ABI diferentes
  • O risco central é que alterar a largura de time_t quebra a ABI
    • Se uma API de biblioteca contém time_t, todo código linkado a essa biblioteca deve usar a mesma largura de tipo
    • Migrar apenas uma parte para time64 não é seguro

LFS e as três sub-ABIs de 32 bits

  • Arquiteturas de 32 bits já tinham, há tempos, um problema de largura de tipos relacionados a arquivos
    • off_t é usado para offsets de arquivos
    • ino_t é usado para números de inode
    • Como originalmente tinham largura de 32 bits, arquivos maiores que 2 GiB ou números de inode fora do intervalo de 32 bits causavam problemas
  • Para resolver isso, foi introduzido o Large File Support (LFS)
    • Ele muda off_t e ino_t para variantes de 64 bits
    • Na glibc, ainda hoje isso é opcional
    • Muitos pacotes ativaram LFS no upstream e trataram a quebra de ABI, mas o problema não foi completamente resolvido
  • O suporte time64 da glibc exige o uso de LFS, formando uma estrutura que resolve juntos os problemas de tamanho de arquivo e de tempo
  • Sistemas de 32 bits têm três sub-ABIs
    • ABI antiga: tipos de 32 bits
    • LFS: off_t de 64 bits, ino_t de 64 bits, time_t de 32 bits
    • time64: LFS + time_t de 64 bits
  • Uma única build da glibc é compatível com as três variantes, mas bibliotecas que usam esses tipos em suas APIs não podem misturar as três variantes entre si

Como a mudança de ABI quebra na prática

  • Quando time_t muda de 32 para 64 bits, o layout das estruturas muda
    • Em uma estrutura de exemplo com int a, time_t b, int c nessa ordem, o offset de c difere entre time_t de 32 bits e time_t de 64 bits
    • Misturar binários time32 e time64 pode fazer com que campos errados sejam lidos ou escritos, e até permitir acessos fora dos limites
  • O tamanho de struct stat também varia conforme a ABI
    • Valor padrão da glibc em x86 de 32 bits: 88 bytes
    • LFS: 96 bytes
    • LFS + time64: 108 bytes
  • Mesmo sem usar estruturas, há problemas em argumentos de funções
    • No x86, argumentos de funções são passados pela pilha
    • Se um dos argumentos for time_t, a posição na pilha dos argumentos seguintes muda
  • Em um experimento de exemplo, valores são corrompidos quando um programa time32 é linkado a uma biblioteca recompilada como time64
    • A saída original é a = 1, um valor de tempo correto, c = 3
    • Se apenas a biblioteca for recompilada com -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64, b e c passam a ser interpretados incorretamente
  • Hoje não há uma proteção prática que impeça essa mistura de ABIs, o que pode gerar quebras em tempo de execução e problemas de segurança

Por que a transição no Gentoo é mais difícil

  • Distribuições binárias recompilam todos os pacotes e então o usuário faz o upgrade em uma etapa relativamente atômica
    • Pode haver problemas com repositórios de terceiros ou programas compilados localmente, mas o processo como um todo é relativamente seguro
  • O Gentoo precisa mudar a ABI no próprio sistema enquanto recompila @world
    • ABIs incompatíveis podem se misturar entre a recompilação de dois pacotes
    • Falhas em algumas recompilações podem deixar o sistema em um estado parcialmente migrado
    • Por causa de dependências circulares, recompilar um pacote dependente pode quebrar ferramentas de build e tornar impossível continuar a recompilação

Mitigações em avaliação

  • Há três direções em discussão
    • Alterar CHOST, a tupla de plataforma, para distinguir a nova ABI da ABI de 32 bits existente
    • Alterar o libdir da nova ABI para instalar bibliotecas recompiladas separadamente das bibliotecas antigas
    • Introduzir uma distinção de ABI em nível binário para impedir que binários de sub-ABIs diferentes sejam linkados
  • Os três métodos podem ser implementados de forma um tanto independente, mas alguns podem depender uns dos outros
  • As strings de exemplo no texto podem não ser as strings reais da solução final

Distinguindo a ABI com CHOST

  • A tupla de plataforma identifica a plataforma-alvo da toolchain e, no Gentoo, também é usada para distinguir ABIs de forma única para suporte a multilib
  • A tupla é composta por quatro partes: arquitetura, vendor, sistema operacional e libc
    • Ex.: i386-pc-linux-gnu
    • Ex.: i686-pc-linux-gnu
    • Ex.: i686-unknown-linux-gnu
  • Ao introduzir uma nova ABI, já se usou a mudança do campo vendor ou o acréscimo de uma marcação de ABI ao campo libc
    • No ABI ARM hardfloat, no passado foram usadas formas como armv7a-hardfloat-linux-gnueabi e armv7a-unknown-linux-gnueabihf
  • Há opções semelhantes para a ABI time64
    • i686-gentoo_t64-linux-gnu
    • i686-pc-linux-gnut64
    • armv7a-gentoo_t64-linux-gnueabihf
    • armv7a-unknown-linux-gnueabihft64
  • A mudança da tupla parece não exigir muitos patches
    • A GNU toolchain e o GNU build system ignoram o conteúdo após gnu no campo libc
    • O Clang precisa de um patch para selecionar automaticamente a ABI correta conforme a tupla

Mudança de libdir e preserved-libs

  • libdir é o nome padrão do diretório de instalação de bibliotecas
    • O padrão comum é lib
    • Em arquiteturas correspondentes de 64 bits, convencionou-se usar frequentemente lib64
    • A ABI x32 do x86 usa libx32, e a ABI n32 do MIPS usa lib32
  • Para a ABI de 32 bits com time64, está em avaliação mudar o libdir de lib para algo como libt64
  • Um libdir separado ajuda a reduzir a mistura de ABIs durante a transição
    • Reduz o risco de um executável time64 ser linkado acidentalmente a uma biblioteca time32
    • Permite preservar bibliotecas time32 com o recurso preserved-libs do Portage
    • Opcionalmente, é possível oferecer perfis multilib time32 + time64 para manter compatibilidade com aplicações time32 pré-compiladas antigas
  • Com preserved-libs, executáveis existentes continuam usando bibliotecas time32 até serem recompilados, enquanto bibliotecas recompiladas como time64 são instaladas no novo libdir
  • A mudança de libdir exige patches na toolchain
    • A glibc pode receber tratamento especial, porque o mesmo conjunto de bibliotecas é válido para várias sub-ABIs
    • Pode ser necessário um ld.so separado para que o .interp de executáveis time64 aponte para o ld.so de time64
  • Para suporte multilib adequado, também é necessária uma tupla de plataforma exclusiva para essa ABI

Marcação de incompatibilidade em nível binário

  • Ao misturar binários de ABIs diferentes, em geral o linker ou o carregador dinâmico deve bloquear a operação
    • Se você linkar um programa de 64 bits a uma biblioteca de 32 bits, o linker rejeita com file in wrong format
    • O carregador dinâmico também rejeita com erros como wrong ELF class: ELFCLASS32
  • A distinção de ABIs existente usa vários mecanismos
    • ELFCLASS32 e ELFCLASS64
    • Machine identifiers como EM_386 e EM_X86_64
    • O campo flags em ARM e MIPS
    • Seções de atributos específicas de arquitetura
  • time32 e time64 precisam de um mecanismo semelhante, mas isso não é simples
    • Não parece haver um mecanismo geral reutilizável
    • É necessária uma solução adequada para várias arquiteturas
    • Adicionar uma nova seção ELF note e implementar suporte na toolchain parece um candidato realista
  • Também é preciso considerar a possibilidade de o usuário desativar a proteção
    • Se um software pré-compilado sem código-fonte não chama APIs que usam time_t, ele pode continuar funcionando com as bibliotecas do sistema
    • Bloquear isso incondicionalmente pode ser uma solução pior que o problema
  • Usar um libdir separado permite criar uma verificação de QA não fatal relativamente simples
    • Distinguir executáveis time64 pelo .interp
    • Verificar se programas time32 não carregam bibliotecas de libt64
    • Verificar se programas time64 não carregam bibliotecas diretamente de lib

Limites das aplicações pré-compiladas de 32 bits

  • Além dos pacotes compilados a partir do código-fonte, há aplicações antigas disponibilizadas apenas como binários pré-compilados para x86 e PowerPC
    • Isso vale especialmente para software proprietário e jogos antigos
  • Elas sofrem tanto com problemas de compatibilidade com bibliotecas do sistema quanto com o próprio problema de 2038
  • Para o problema de compatibilidade, a estrutura multilib existente oferece parte da solução
    • No amd64, já existem um layout multilib e mecanismos para compilar várias versões de bibliotecas a fim de dar suporte a software de 32 bits
    • Isso pode ser estendido distinguindo abi_x86_32 e abi_x86_t64
    • É possível criar um novo perfil multilib x86 que ofereça suporte às duas ABIs
  • A falha dos próprios programas de 32 bits após 2038 continua sendo mais difícil
    • Uma opção é controlar o horário do sistema com faketime
    • Outra opção é executar uma VM com o tempo ajustado para o passado

Correção de 2024-09-30: apenas libdir não basta

  • A ideia inicial era otimista demais, e mudar apenas o libdir dificulta uma separação estável
  • Como todos os libdirs são listados em ld.so.conf, não dá para depender de caminhos de libdir codificados diretamente dentro do ld.so
    • Prefixos customizados do LLVM já ajustam caminhos, e esse caso também exigiria tratamento especial
  • Com isso, aumenta a probabilidade de a mudança de libdir depender de uma distinção de incompatibilidade binária
  • Há três objetivos básicos que precisam ser cumpridos
    • O carregador dinâmico deve distinguir binários time32 e time64
    • Todo binário sem marcação time64 explícita deve ser considerado time32, para compatibilidade retroativa
    • Todos os binários recém-compilados devem ter uma marcação time64 explícita, incluindo binários compilados em ambientes não C, como Rust
  • Esse objetivo exige patches em várias toolchains de várias linguagens
    • Seria difícil para o Gentoo manter isso apenas localmente, e é necessária cooperação de várias partes
    • As arquiteturas-alvo em geral são consideradas legadas ou já não recebem suporte suficiente
  • Outra questão é se outras toolchains vão gerar executáveis time64 corretos
    • Se não forem ajustadas para seguir _TIME_BITS como programas em C, elas podem codificar uma largura específica de time_t e quebrar
  • Como todo binário sem marcação time64 explícita passaria a usar bibliotecas time32, o Gentoo não conseguiria executar executáveis de terceiros que não tenham sido corrigidos para receber a marcação correta
  • Também se considera uma alternativa com objetivo mais baixo
    • Injetar RPATH em todos os executáveis time64 para forçar diretamente o libdir time64
    • Esse método não impede completamente o carregador dinâmico de usar bibliotecas time32, mas pode ajudar na transição sem grandes problemas de compatibilidade
  • Por outro lado, também há a opção de não mudar permanentemente o libdir time64 e mudar temporariamente o libdir time32
    • Injetar RPATH nos programas existentes e renomear o libdir
    • Instalar as novas bibliotecas time64 no libdir existente
    • Novos programas time64 não teriam um RPATH que força bibliotecas time32
    • A vantagem é manter compatibilidade com outras distribuições que já fizeram a transição

Tarefas restantes

  • Implementar as três soluções pode oferecer um caminho de transição mais limpo e relativamente seguro para sistemas Gentoo de 32 bits que usam glibc
  • Porém, essas soluções se aplicam principalmente a pacotes compilados a partir do código-fonte
  • Aplicações pré-compiladas de 32 bits ainda ficam com o problema de 2038, mesmo que a compatibilidade de ABI seja mantida
  • O design geral ainda é um rascunho, e pode continuar mudando conforme experimentos, discussões e envio de patches

1 comentários

 
GN⁺ 2024-09-30
Comentários do Hacker News
  • No Gentoo há algumas opções que o texto não aborda, e parecem ter ficado de fora porque, pelo desenho do sistema do Gentoo, a carga de trabalho seria grande

    1. Permitir compilar um pacote como alvo sem instalá-lo. O ponto central é que, no Gentoo, compilação e instalação de pacotes são uma etapa só, então não dá para compilar antes vários itens que dependem entre si e depois instalar os resultados de forma atômica. Durante atualizações com mudança de ABI, o sistema tende a ficar parcialmente quebrado com facilidade
    2. Estender o versionamento normal de .so para refletir também mudanças de ABI dos pacotes dependentes. Em geral, bibliotecas compartilhadas têm números de versão no nome do arquivo e na versão interna, como libfoo.so.1.0.0, e o pacote rastreia suas próprias quebras de ABI. Para suportar time_t de 64 bits, seria preciso adicionar a cada .so um elemento de versão controlado pela ABI dependente. O resultado seria parecido com o “usar outro libdir” do texto, mas isso talvez fosse muito mais intrusivo, embora pudesse virar uma base reutilizável para futuras mudanças de ABI
    • Para “compilar sem instalar”, o que parece encaixar melhor é uma atualização parcial em etapas
      Você agenda a compilação de vários pacotes novos e os compila em um sandbox; depois, as novas compilações podem ser feitas de modo que, via union, olhem primeiro para o sandbox e só depois façam fallback para o sistema. Quando tudo estiver compilado, basta empacotar os resultados e movê-los do sandbox para o sistema real. Isso transformaria toda a atualização do Gentoo em algo parecido com uma transação, o que traria grandes vantagens também em outros aspectos
    • O Gentoo já suporta o item 1 ao permitir definir um diretório de destino para a instalação da imagem pronta. Normalmente isso fica em /, então basta mudar o ROOT
      É possível recompilar todo o @system e @world e instalar no subdiretório indicado, depois sincronizando tudo de uma vez. Se possível, é melhor fazer isso a partir de uma sessão live; em teoria, também dá para fazer bind mount de / em um subdiretório do novo local de instalação, entrar com chroot e sincronizar com o / superior real
      https://devmanual.gentoo.org/ebuild-writing/variables/#root
    • O Gentoo já preserva as bibliotecas antigas até que todas as dependências sejam atualizadas, então esse problema pode ser resolvido codificando a mudança de ABI na arquitetura e no SONAME, que é onde originalmente a mudança de ABI deveria ser registrada
  • A forma como o Mac OS X tratou off_t e ino_t pode dar uma pista. As chamadas e estruturas antigas continuaram funcionando, e foram adicionadas novas chamadas e tipos com sufixo 64; macros do pré-processador podiam escolher para onde o nome realmente apontava, embora fosse raro escrever isso diretamente
    Em compensação, o OS e o SDK têm versionamento, e no build você pode especificar a versão mais antiga do sistema operacional na qual o binário precisa rodar. Os headers escolhiam automaticamente os macros adequados com base nisso, e anotações de APIs novas/obsoletas usavam o mesmo mecanismo para gerar weak linking ou avisos. No começo isso era feito no pré-processador, mas hoje o compilador entende com mais precisão o que a Apple chama de disponibilidade de API, então parece viável fazer algo parecido em outras plataformas

    • Isso não resolve o problema principal explicado pela TFA. Aplicações que usam versões diferentes de “versão de OS alvo de compilação” deixam de conseguir fazer link entre si
      Mesmo rodando no OS v.B, a aplicação X, declarada para o OS v.B, pode não conseguir fazer link com a aplicação Y, declarada para o OS v.A. Na prática, essa abordagem é bem próxima do que quase todas as plataformas já fazem, e fazer diferente quebraria imediatamente a compatibilidade binária existente
    • Bibliotecas auxiliares que não sejam a glibc não vão definir múltiplos conjuntos de funções por tamanho de off_t, nem vão ter nos headers uma chave que escolha de forma transparente o conjunto correto de funções conforme o tamanho de tipo desejado pelo programa cliente
      Ainda assim, o texto enfatiza que time_t é um problema maior do que off_t. Uma razão plausível é que time_t está muito mais espalhado. off_t é um tipo POSIX envolvido em relativamente poucas interfaces, enquanto time_t faz parte da ISO C e aparece por toda parte. Além disso, muito código em C assume que time_t é um tipo inteiro com a mesma largura de int, algo menos comum no caso de off_t
    • Parece uma solução elegante, mas na prática soa como um hack terrível. Macros sem tipo são um pesadelo com o qual eu nunca mais quero lidar
    • Só funciona quando você pode forçar toda a plataforma a acompanhar. É uma boa solução, mas exige controle sobre a biblioteca C. O Gentoo não controla o que a libc faz, e o usuário pode usar GNU libc, musl ou outra coisa
  • No Debian também foi muito doloroso. Alguns provavelmente chegaram ao burnout, e muita gente apontava para distribuições baseadas em código-fonte dizendo “lá deve ser facílimo”

    • Tenho curiosidade sobre materiais que mostrem em detalhe como a transição para time64 foi dolorosa no Debian. Vendo de fora, pareceu relativamente tranquila e pouco controversa, por exemplo muito melhor do que a fusão de /usr
    • Fiz as transições de m68k, powerpc e sh4, e também ajudei um pouco com hppa; com a ajuda de outros desenvolvedores do Debian, ainda estou vivo
    • Se “fácil” quiser dizer “é só mandar o usuário recompilar tudo de uma vez”, então provavelmente sim
  • Sempre que vejo gente sofrendo com esse tipo de problema, penso como foi realmente uma sorte termos forçado isso na época do porte inicial do amd64 no FreeBSD. Foi possível definir os tipos básicos da ABI e decidir olhar para o futuro em vez de para o passado
    O amd64 tinha uma característica interessante que facilitou esse trabalho. Como argumentos de função de 32 bits eram convertidos automaticamente para 64 bits durante a chamada, mesmo passando um inteiro de tempo de 32 bits para uma função que esperava time_t de 64 bits, na maior parte dos casos simplesmente funcionava durante o trabalho inicial da plataforma. Assim, os detalhes menores puderam ficar para depois
    Havia outras plataformas de 64 bits na época, mas sem time_t de 64 bits, e o FreeBSD/amd64 foi o primeiro daquela família por volta de 2003~2005. Pelo que lembro, o sparc64 também migrou para time_t de 64 bits
    O maior problema era que o tzcode não era seguro para 64 bits naquela época. O algoritmo de normalização de struct tm caía em condições degeneradas, tentando calcular iterativamente dia/mês/ano de time_t(2^62). Em vez de alterar bastante o tzcode, pelo que lembro foi tratado para falhar de forma aproximada antes de 1900 ou depois de 10000. É bem possível que isso já tenha sido corrigido no upstream há muito tempo
    Durante alguns anos, lidamos no estilo jogo de acertar toupeira com confusões entre tempo de 32/64 bits causadas por código de terceiros tratando int/long/time_t de forma descuidada em estruturas de dados de arquivo ou de rede, mas no geral não foi um grande problema. Usar time_t de 64 bits desde o primeiro dia evitou a maior parte dos problemas, e fazer isso desde o início era fácil. O Linux perdeu uma grande oportunidade de fazer o mesmo ao iniciar o porte para amd64/x86_64
    Como observação adicional, na época não conseguimos concluir o ino_t de 64 bits. Números de inode de 32 bits estavam expostos em muitos lugares, como estruturas on-disk do sistema de arquivos, estruturas de diretório do UFS etc. Naquele momento, quando o FreeBSD/amd64 ainda era uma plataforma de nível inferior, não havia um jeito realista de resolver isso desde o início sem abalar bastante outras arquiteturas tier-1. O trabalho foi feito duas vezes, mas no fim outra pessoa concluiu, corrigindo junto constantes que eram curtas demais, como o comprimento do caminho de mountpoint

    • Pelo que entendo, todos os portes Linux de 64 bits usaram time_t, off_t e ino_t de 64 bits desde o início. O problema agora é migrar o Linux de 32 bits para time_t de 64 bits
    • O FreeBSD também foi mais agressivo com off_t, que passou a ser de 64 bits desde o 2.0. Ainda restam vestígios do tamanho antigo nas versões de 32 bits do Linux
      Entendo que essa parte de os argumentos de função de 32 bits serem convertidos automaticamente para 64 bits durante a chamada só funciona para argumentos sem sinal. Ao carregar em %edi, a parte superior de %rdi é zerada por isso. A especificação da ABI SysV para x86-64 não diz que todos os valores em registradores ou na pilha são estendidos para o valor completo de 64 bits, e até a observação sobre booleanos diz que só o byte inferior é significativo, sugerindo que essa é a regra geral
    • Se isso quer dizer que, quando o amd64 apareceu, o FreeBSD também portou o time_t do i386 para 64 bits, é bem surpreendente. Fico curioso se outras arquiteturas de 32 bits, como Motorola 68000 ou sparc32, também migraram para time_t de 64 bits
  • Em um grande sistema Unix antigo de 32 bits, para lidar com datas futuras, já substituí funções da libc com time_t assinado de 32 bits por equivalentes com time_t sem sinal de 32 bits. Isso rendeu mais 68 anos depois de 2038, e até lá eu já não estarei mais aqui
    A desvantagem é não poder representar datas anteriores ao epoch Unix de 1970, mas como era um sistema de agenda, isso não era problema. Se datas passadas forem importantes, também daria para deslocar o epoch algumas décadas ou reduzir a resolução temporal de 1 segundo para 2 segundos. Cada abordagem tem seus problemas sutis, então depende do caso de uso

    • Se era possível mudar o sistema inteiro de com sinal para sem sinal, fico pensando por que não mudar para 64 bits
  • Na página de manual original do BSD, a seção “Bugs” do tunefs tinha a piada famosa: “You can tune a file system, but you can't tune a fish.”, e segundo “Expert C Programming”, no código-fonte dessa página de manual havia este comentário ao lado da piada
    “Se você tirar isso, o daemon do UNIX seguirá seus passos a partir de agora até o momento em que time_t der wrap around.”
    Quando essa frase foi escrita nos anos 70, 2038 evidentemente era um futuro inimaginavelmente distante
    https://progforperf.github.io/Expert_C_Programming.pdf

  • A principal sensação que isso me deixou foi: respeito o esforço, mas como usuário eu só queria migrar para uma distribuição não baseada em código-fonte como o Debian e encerrar esse problema

    • A dificuldade de distribuições baseadas em código-fonte parece vir de tentar fazer um upgrade in-place enquanto se faz uma mudança incompatível de ABI. Por isso, trocar para uma distribuição totalmente diferente pode ser tão disruptivo quanto, ou talvez até menos demorado que, fazer uma instalação limpa do Gentoo com a nova ABI
    • No Gentoo também há um jeito simples de lidar com isso. Dá para iniciar por USB ou algo do tipo, executar mkfs.ext4 ou o sistema de arquivos usado nas partições / e /usr, montar, extrair o stage3, entrar em chroot e executar emerge $all-my-packages-that-where-installed-before-mkfs
      Em vez de atualizar aos poucos, você pode instalar uma nova cópia do Gentoo
    • Essa distinção de que bastaria mudar para uma distribuição não baseada em código-fonte é um pouco mais sutil. Distribuições baseadas em código-fonte como o NixOS não têm o mesmo problema. A questão central está menos em compilar a partir do código-fonte e mais em como o Gentoo constrói e instala pacotes
      Se houver software proprietário de terceiros, ainda podem surgir problemas mesmo em sistemas binários. Também pode haver problemas em pacotes de primeira parte instalados separadamente em etapas independentes
  • Não sou especialista em C, mas achava que aliases de tipo como off_t tinham sido introduzidos justamente para poderem ser trocados depois. Mas isso não parece funcionar de forma clara, então fico na dúvida se entendi errado

    • É a diferença entre compatibilidade de código-fonte e compatibilidade binária. Usar typedefs como off_t normalmente significa que você não precisa reescrever o código, mas precisa recompilar tudo que usa esse tipo
    • Até certo ponto funciona, mas não combina bem com distribuições baseadas em código-fonte. Se, depois de mudar a definição de off_t, fosse possível reconstruir @world atomicamente, não haveria problema; mas distribuições baseadas em código-fonte não recompilam @world de forma atômica, e sim os pacotes um por um
      Aí podem surgir problemas como libc.so usando off_t de 64 bits enquanto o gcc foi compilado com base em off_t de 32 bits, fazendo o gcc travar. Pacotes necessários para reconstruir @world, como bash, coreutils, make e binutils, também podem quebrar, e nesse ponto você fica bloqueado. Por isso upgrades desse tipo exigem cuidado
    • Isso é só a primeira parte do quebra-cabeça, talvez nem isso, só meia parte. Como o texto diz, no momento em que off_t entra em uma struct, é usado em chamadas de função ou é incorporado a um protocolo, a abstração desaparece e o tamanho real passa a importar
      Se você misturar código antigo e novo ao carregar bibliotecas ou se comunicar por protocolo, os offsets começam a sair do lugar e os crashes começam a acontecer. No fim, a transição exige que todos separem seus programas em “legado” e “portado ou pelo menos revisado”, o que é extremamente doloroso
    • Aliases de tipo só facilitam as coisas no nível do código-fonte. Não são uma abstração de verdade, muito menos completa. Por exemplo, se você trocar o tipo interno por um tipo de ponto flutuante, o significado muda muito e isso fica totalmente exposto no código do usuário
      Mesmo ao trocar por um tipo maior com significado parecido, ainda pode quebrar. Um exemplo simples é padding em struct, e também há muitos usos em que ponteiros são convertidos para inteiros e depois de volta, então, se a representação interna mudar, a quebra é inevitável. Se isso é uma boa prática ou não é outra questão, mas está longe de ser raro. O ponto central é a compatibilidade de ABI
    • Funciona, mas a mudança de ABI tem o problema de que, ao mudar, tudo precisa mudar ao mesmo tempo. Basicamente, não existe nenhum mecanismo que impeça você de linkar bibliotecas compiladas com off_t de 32 bits com bibliotecas compiladas com off_t de 64 bits, e o comportamento resultante pode ser extremamente imprevisível
  • No exemplo da struct, foi dito que o offset de c é 8 quando time_t é de 32 bits e 12 quando o tipo é de 64 bits, mas na verdade não deveria ser 16? Como b precisa estar alinhado em 64 bits, deveria haver padding entre a e b. Na verdade isso até reforça ainda mais o argumento que o autor quer fazer

    • A maioria das ABIs x86 não impõe padding para tipos de 64 bits porque, na época, não havia carregamentos de 64 bits
  • Vendo tudo isso, dá para dizer que a representação estranha de tempo do Windows — contar em 64 bits com unidades de 100 ns desde 00:00 GMT de 1º de janeiro de 1601 no calendário gregoriano — até tem uma pequena vantagem. A resolução também é excelente e vai continuar funcionando até a conquista da galáxia inteira