ii. Observações Técnicas do Conjunto de Ferramentas

Esta seção explica algumas das razões e detalhes técnicos por trás do método completo de construção. Não tente imediatamente entender tudo nesta seção. A maior parte desta informação ficará mais clara depois de realizar uma construção real. Volte e releia este capítulo a qualquer tempo durante o processo de construção.

O objetivo geral do Capítulo 5 e do Capítulo 6 é o de produzir uma área temporária contendo um conjunto de ferramentas que sejam conhecidas por serem boas e que estejam isoladas do sistema anfitrião. Usando-se o comando chroot, as compilações nos capítulos subsequentes estarão isoladas naquele ambiente, assegurando uma construção limpa e livre de problemas do sistema LFS alvo. O processo de construção foi projetado para minimizar os riscos para novos(as) leitores(as) e para fornecer o maior valor educacional ao mesmo tempo.

O processo de construção é baseado em compilação cruzada. Compilação cruzada normalmente é usada para construir um compilador e o conjunto de ferramentas associadas dele para uma máquina diferente daquela que é usada para a construção. Isso não é estritamente necessário para o LFS, pois a máquina onde o novo sistema executará é a mesma que aquela usada para a construção. Porém, a compilação cruzada tem a grande vantagem: tudo o que for compilado cruzadamente não pode depender do ambiente do anfitrião.

Acerca da Compilação Cruzada

[Nota]

Nota

O livro LFS não é (e não contém) um tutorial geral para construir um conjunto cruzado de ferramentas (ou nativo). Não use os comandos no livro para um conjunto cruzado de ferramentas para algum outro propósito que não o de construir o LFS, a menos que você realmente entenda o que está fazendo.

É sabido que instalar o GCC passagem 2 quebrará o conjunto cruzado de ferramentas. Nós não consideramos isso um defeito porque o GCC passagem 2 é o último pacote a ser compilado cruzadamente no livro, e não o consertaremos até que realmente precisemos compilar cruzadamente algum pacote depois do GCC passagem 2 no futuro.

Compilação cruzada envolve alguns conceitos que merecem uma seção por si próprios. Embora esta seção possivelmente seja omitida em uma primeira leitura, retornar até ela posteriormente te ajudará a ganhar um entendimento mais completo do processo.

Permita-nos primeiro definir alguns termos usados nesse contexto.

A construtora

é a máquina onde nós construímos programas. Observe que essa máquina também é referenciada como a anfitriã.

A anfitriã

é a máquina/sistema onde os programas construídos executarão. Observe que esse uso de host não é o mesmo que em outras seções.

O alvo

é usado somente para compiladores. Ele é a máquina para a qual o compilador produz código. Ele possivelmente seja diferente tanto da construtora quanto da anfitriã.

Como um exemplo, permita-nos imaginar o seguinte cenário (ocasionalmente referenciado como Cruzado Canadense). Nós temos um compilador somente em uma máquina lenta, vamos chamá-la de máquina A, e o compilador de ccA. Nós também temos uma máquina rápida (B), porém nenhum compilador para (B), e nós queremos produzir código para uma terceira, máquina lenta (C). Nós construiremos um compilador para a máquina C em três estágios.

Estágio Construtora Anfitriã Alvo Ação
1 A A B Construir compilador cruzado cc1 usando ccA na máquina A.
2 A B C Construir compilador cruzado cc2 usando cc1 na máquina A.
3 B C C Construir compilador ccC usando cc2 na máquina B.

Então, todos os programas necessários para a máquina C podem ser compilados usando cc2 na rápida máquina B. Observe que a menos que B consiga executar programas produzidos por C, não existe maneira de testar os programas recém construídos até que a própria máquina C esteja executando. Por exemplo, para executar uma suíte de teste em ccC, nós possivelmente queiramos adicionar um quarto estágio:

Estágio Construtora Anfitriã Alvo Ação
4 C C C Reconstruir e testar ccC usando ccC na máquina C.

No exemplo acima, somente cc1 e cc2 são compiladores cruzados, isto é, eles produzem código para uma máquina diferente daquela na qual estão executando. Os outros compiladores, ccA e ccC, produzem código para a máquina na qual estão executando. Tais compiladores são chamados de compiladores nativos.

Implementação da Compilação Cruzada para o LFS

[Nota]

Nota

Todos os pacotes compilados cruzadamente neste livro usam um sistema de construção baseado no autoconf. O sistema de construção baseado no autoconf aceita tipos de sistema na forma cpu-vendor-kernel-os, referenciado como o tripleto do sistema. Dado que o campo vendor frequentemente é irrelevante, o autoconf te permite omiti-lo.

Um(a) leitor(a) astuto(a) possivelmente questione porque um tripleto se refere a um nome de quatro componentes. O campo kernel e o campo os iniciaram como um campo único do sistema. Tal forma de três campos ainda é válida atualmente para alguns sistemas, por exemplo, x86_64-unknown-freebsd. Porém, dois sistemas conseguem compartilhar o mesmo núcleo e ainda serem muito diferentes para usarem o mesmo tripleto para descrevê-los. Por exemplo, o Android executando em um telefone móvel é completamente diferente do Ubuntu executando em um servidor ARM64, apesar de ambos estarem executando no mesmo tipo de CPU (ARM64) e usando o mesmo núcleo (Linux).

Sem uma camada de emulação, você não consegue executar um executável para um servidor em um telefone móvel ou vice versa. Assim, o campo system foi dividido nos campos kernel e os para designar esses sistemas inequivocamente. No nosso exemplo, O sistema Android é designado como aarch64-unknown-linux-android e o sistema Ubuntu é designado como aarch64-unknown-linux-gnu.

A palavra tripleto permanece embutida no léxico. Uma maneira simples para se determinar o teu tripleto do sistema é a de executar o conjunto de comandos sequenciais config.guess que vem com o fonte para muitos pacotes. Desempacote os fontes do binutils, execute o conjunto de comandos sequenciais ./config.guess e observe a saída gerada. Por exemplo, para um processador Intel de 32 bits, a saída gerada será i686-pc-linux-gnu. Em um sistema de 64 bits, será x86_64-pc-linux-gnu. Na maioria dos sistemas Linux, o comando ainda mais simples gcc -dumpmachine te dará informação semelhante.

Você também deveria estar ciente do nome do vinculador dinâmico da plataforma, frequentemente referido como o carregador dinâmico (não seja confundido com o vinculador padrão ld que é parte do binutils). O vinculador dinâmico fornecido pelo pacote glibc encontra e carrega as bibliotecas compartilhadas necessárias para um programa, prepara o programa para execução e então o executa. O nome do vinculador dinâmico para uma máquina Intel de 32 bits é ld-linux.so.2; e é ld-linux-x86-64.so.2 em sistemas de 64 bits. Uma maneira infalível para se determinar o nome do vinculador dinâmico é a de inspecionar uma biblioteca aleatória oriunda do sistema anfitrião executando: readelf -l <nome do binário> | grep interpreter e observar a saída gerada. A referência autoritativa cobrindo todas as plataformas está em uma página wiki da Glibc.

Existem dois pontos-chave para uma compilação cruzada:

  • Ao produzir e processar o código de máquina supostamente para ser executado na anfitriã, o conjunto cruzado de ferramentas precisa ser usado. Observe que o conjunto nativo de ferramentas originário da construtora ainda pode ser invocado para gerar código de máquina supostamente para ser executado na construtora. Por exemplo, o sistema de construção pode compilar um gerador com o conjunto nativo de ferramentas, então gerar um arquivo fonte C com o gerador, e finalmente compilar o arquivo fonte C com o conjunto cruzado de ferramentas, de forma que o código gerado estará apto para executar na anfitriã.

    Com um sistema de construção baseado em autoconf, esse requisito é garantido usando-se a chave --host para se especificar o tripleto da anfitriã. Com essa chave, o sistema de construção usará os componentes do conjunto de ferramentas prefixados com <the host triplet> para gerar e processar o código de máquina para a anfitriã; por exemplo, o compilador será <the host triplet>-gcc e a ferramenta readelf será <the host triplet>-readelf.

  • O sistema de construção não deveria tentar executar nenhum código de máquina gerado supostamente para ser executado na anfitriã. Por exemplo, ao construir um utilitário nativamente, a página de manual dele pode ser gerada executando-se o utilitário com a chave --help e processando-se a saída gerada, mas geralmente não é possível fazer isso para uma compilação cruzada, pois o utilitário possivelmente falhe para executar na construtora: obviamente é impossível executar código de máquina ARM64 em uma CPU x86 (sem um emulador).

    Com um sistema de construção baseado em autoconf, esse requisito é satisfeito no modo de compilação cruzada, onde os recursos opcionais que exigem executar código de máquina para a anfitriã durante o tempo de construção são desabilitados. Quando o tripleto da anfitriã for especificado explicitamente, o modo de compilação cruzada é habilitado se e somente se o conjunto de comandos sequenciais configure falhar para executar um programa fictício compilado no código de máquina da anfitriã ou o tripleto da construtora for especificado explicitamente por meio da chave --build e ele for diferente do tripleto da anfitriã.

Para a finalidade de compilar cruzadamente um pacote para o sistema temporário LFS, o nome do tripleto do sistema é ligeiramente ajustado mudando-se o campo "vendor" na variável LFS_TGT, de forma que ele diga "lfs" e LFS_TGT é então especificada como o tripleto da anfitriã via --host, de forma que o conjunto cruzado de ferramentas será usado para gerar e processar o código de máquina executando como parte do sistema temporário LFS. E também nós precisamos habilitar o modo de compilação cruzada: apesar do código de máquina da anfitriã, ou seja, o código de máquina para o sistema temporário LFS, estar apto para executar na CPU real, ele possivelmente se refira a uma biblioteca não disponível na construtora (a distribuição da anfitriã), ou algum código ou dados não existir ou ser definido diferentemente na biblioteca, mesmo se acontecer de estar disponível. Ao compilar cruzadamente um pacote para o sistema temporário LFS, nós não podemos confiar no conjunto de comandos sequenciais configure para detectar esse problema com o programa fictício: o fictício usa somente alguns componentes na libc que a libc da distribuição da anfitriã provavelmente fornece (a menos que, talvez, a distribuição da anfitriã use uma implementação da libc diferente, como Musl), de forma que ele não falhará como os programas realmente úteis provavelmente falhariam. Portanto, nós precisamos especificar explicitamente o tripleto da construtora para habilitar o modo de compilação cruzada. O valor que nós usamos é exatamente o padrão, ou seja, o tripleto original do sistema proveniente da saída gerada do config.guess, mas o modo de compilação cruzada depende de uma especificação explícita, como nós discutimos.

Nós usamos a opção --with-sysroot ao construir o vinculador cruzado e o compilador cruzado, para dizer a eles onde encontrar os arquivos necessários para a anfitriã. Isso quase garante que nenhum dos outros programas construídos no Capítulo 6 consiga vincular-se a bibliotecas na construtora. A palavra quase é usada porque libtool, um envolucrador de compatibilidade do compilador e do vinculador para sistemas de construção baseados em autoconf, pode tentar ser muito inteligente e passar erroneamente opções permitindo que o vinculador encontre bibliotecas da construtora. Para evitar essa precipitação, nós precisamos deletar os arquivos de arquivamento da libtool (.la) e consertar uma cópia desatualizada da libtool enviada com o código do Binutils.

Estágio Construtora Anfitriã Alvo Ação
1 pc pc lfs Construir compilador cruzado cc1 usando cc-pc em pc.
2 pc lfs lfs Construir compilador cc-lfs usando cc1 em pc.
3 lfs lfs lfs Reconstrua (e talvez teste) cc-lfs usando cc-lfs no lfs.

Na tabela precedente, em pc significa que os comandos são executados em uma máquina usando a distribuição já instalada. Em lfs significa que os comandos são executados em um ambiente chroot.

Esse não é ainda o fim da estória. A linguagem C não é meramente um compilador; ela também define uma biblioteca padrão. Neste livro, a biblioteca GNU C, chamada de glibc, é usada (existe uma alternativa, "musl"). Essa biblioteca precisa ser compilada para a máquina LFS; isto é, usando o compilador cruzado cc1. Porém, o próprio compilador usa uma biblioteca interna fornecendo sub rotinas complexas para funções não disponíveis no conjunto de instruções do montador. Essa biblioteca interna é chamada de libgcc e ela precisa ser lincada à biblioteca glibc para ser completamente funcional. Além disso, a biblioteca padrão para C++ (libstdc++) também precisa estar lincada com a glibc. A solução para esse problema de ovo e galinha é a de primeiro se construir uma libgcc degradada baseada em cc1, carecendo de algumas funcionalidades, tais como camadas e manuseio de exceções, e então se construir a glibc usando esse compilador degradado (a glibc em si não é degradada), e também se construir a libstdc++. Essa última biblioteca carecerá de algumas das funcionalidades da libgcc.

O resultado do parágrafo precedente é o de que cc1 está inapto para construir uma libstdc++ totalmente funcional com a libgcc degradada, mas cc1 é o único compilador disponível para se construir as bibliotecas C/C++ durante o estágio 2. Conforme nós discutimos, não podemos executar cc-lfs na pc (a distribuição da anfitriã) porque ele possivelmente exija alguma biblioteca, código ou dados não disponível na construtora (a distribuição da anfitriã). Então, quando nós construímos o estágio 2 do gcc, substituímos o caminho de pesquisa da biblioteca para lincar libstdc++ à libgcc recém-reconstruída em vez da construção antiga e degradada. Isso torna a libstdc++ reconstruída totalmente funcional.

No Capítulo 8 (ou estágio 3), todos os pacotes necessários para o sistema LFS são construídos. Mesmo se um pacote já tiver sido instalado no sistema LFS em um capítulo anterior, nós ainda reconstruímos o pacote. O principal motivo para se reconstruir esses pacotes é para torná-los estáveis: se reinstalarmos um pacote do LFS em um sistema LFS concluído, o conteúdo reinstalado do pacote deveria ser o mesmo que o conteúdo do mesmo pacote quando instalado pela primeira vez no Capítulo 8. Os pacotes temporários instalados no Capítulo 6 ou no Capítulo 7 não conseguem satisfazer essa exigência, porque alguns recursos opcionais deles são desabilitados devido ou às dependências ausentes ou ao modo de compilação cruzada. Além disso, um motivo secundário para se reconstruir os pacotes é para executar as suítes de teste.

Outros Detalhes Procedurais

O compilador cruzado será instalado em um diretório $LFS/tools separado, pois ele não será parte do sistema final.

Binutils é instalado primeiro, pois as execuções do configure do gcc e da glibc realizam vários testes de recursos no montador e no lincador para determinar quais recursos de software habilitar ou desabilitar. Isso é mais importante do que, inicialmente, alguém possa perceber. Um gcc ou uma glibc configurado incorretamente pode resultar em um conjunto de ferramentas sutilmente quebrado, onde o impacto de tal quebra talvez não se manifeste até próximo do final da construção de uma distribuição inteira. Uma falha de suíte de teste normalmente destacará tal erro antes que muito mais trabalho adicional seja realizado.

O Binutils instala o montador e o lincador dele em dois locais, $LFS/tools/bin e $LFS/tools/$LFS_TGT/bin. As ferramentas em um local são rigidamente lincadas às outras. Uma faceta importante do lincador é a ordem dele de procura de biblioteca. Informação detalhada pode ser obtida a partir do ld passando-lhe o sinalizador --verbose. Por exemplo, $LFS_TGT-ld --verbose | grep SEARCH ilustrará os caminhos atuais de procura e a ordem deles. (Observe que esse exemplo consegue ser executado conforme mostrado somente enquanto logado(a) como usuário(a) lfs. Se você retornar a esta página posteriormente, substitua $LFS_TGT-ld por ld).

O próximo pacote instalado é gcc. Um exemplo do que pode ser visto durante a execução dele do configure é:

checking what assembler to use... /mnt/lfs/tools/i686-lfs-linux-gnu/bin/as
checking what linker to use... /mnt/lfs/tools/i686-lfs-linux-gnu/bin/ld

Isso é importante pelas razões mencionadas acima. Também demonstra que o conjunto de comandos sequenciais de configuração do gcc não procura nos diretórios do PATH para encontrar quais ferramentas usar. Entretanto, durante a operação real do gcc em si, os mesmos caminhos de procura não são necessariamente usados. Para descobrir qual lincador padrão o gcc usará, execute: $LFS_TGT-gcc -print-prog-name=ld. (Novamente, remova o prefixo $LFS_TGT- se retornar a isso posteriormente.)

Informação detalhada pode ser obtida a partir do gcc passando-se a opção de linha de comando -v enquanto compilar um programa. Por exemplo, $LFS_TGT-gcc -v example.c (ou sem $LFS_TGT- se retornar posteriormente) exibirá informação detalhada acerca do preprocessador, compilação e estágios da montagem, incluindo os caminhos de procura do gcc para cabeçalhos inclusos e a ordem deles.

Em seguida: cabeçalhos sanitizados da API do Linux. Eles permitem que a biblioteca C padrão (glibc) interaja com os recursos que o núcleo Linux fornecerá.

Em seguida vem glibc. Esse é o primeiro pacote que nós compilamos cruzadamente. Nós usamos a opção --host=$LFS_TGT para fazer o sistema de construção usar aquelas ferramentas prefixadas com $LFS_TGT-, e a opção --build=$(../scripts/config.guess) para habilitar o modo de compilação cruzada como discutimos. A variável DESTDIR é usada para forçar a instalação no sistema de arquivos do LFS.

Conforme mencionado acima, a biblioteca padrão C++ é compilada em seguida, seguida no Capítulo 6 por outros programas que precisam ser compilados cruzadamente para quebrar dependências circulares ao tempo da construção. As etapas para aqueles pacotes são semelhantes às etapas para glibc.

No final do Capítulo 6 o compilador nativo do LFS é instalado. Primeiro binutils-pass2 é construído, no mesmo diretório DESTDIR que os outros programas, então a segunda passagem de gcc é construída, omitindo-se algumas bibliotecas não críticas.

Uma vez dentro do ambiente chroot no Capítulo 7, as instalações temporárias de programas necessários para a operação apropriada do conjunto de ferramentas são realizadas. Deste ponto em diante, o conjunto central de ferramentas está auto-contido e auto-hospedado. No Capítulo 8, as versões finais de todos os pacotes necessários para um sistema completamente funcional são construídas, testadas e instaladas.