O Mac OS X utiliza o formato Mach-O (Mach Object) para binários. Um binário do tipo Universal Binary (Binário Universal) ou também chamado de Fat Binary (Binário Gordo) na época do NeXTStep, é um formato de arquivo executável que pode conter código de uma ou mais arquiteturas.
Vale comentar que até o Leopard, o Mac OS X inclua código para executar tanto em uma máquina Intel (i386) quanto em um PowerPC (ppc). O Snow Leopard marcou o fim do suporte a arquitetura PowerPC, desta forma os binários dessa versão são apenas para arquitetura Intel de 32 e 64 bits.
$ lipo -i /bin/ls Architectures in the fat file: /bin/ls are: x86_64 i386
Mesmo com esta mudança de rumo do Mac OS X, ainda é possível encontrar binários com três arquiteturas distintas, duas Intel e um PowerPC, como por exemplo no código do XCode.
$ lipo -i /Developer/Applications/Xcode.app/Contents/MacOS/Xcode Architectures in the fat file: /Developer/Applications/Xcode.app/Contents/MacOS/Xcode are: x86_64 i386 ppc7400
Tipicamente um compilador gera por padrão o código para uma CPU (target). Vejamos como o GCC se comporta sem passar opções especiais a ele.
$ cat hello.c
main() { puts("Hello World!"); }
$ gcc hello.c -o hello
$ ./hello
Hello World!
$ file hello
hello: Mach-O executable i386
$ lipo -i hello
Non-fat file: hello is architecture: i386
O GCC gera código para a arquitetura corrente da máquina, se estivesse no Mac OS X em 64 bits o resultado seria x86_64. Note que este binário não é universal (Non-fat).
No XCode a opção de compilar código Universal é selecionável por configuração. Para o GCC, os parâmetros para construir um binário universal são -arch i386 e -arch x86_64, conforme mostra o exemplo:
$ gcc -arch i386 -arch x86_64 hello.c -o hello $ lipo -i hello Architectures in the fat file: hello are: i386 x86_64
Com este binário, pode-se executá-lo tanto em Intel 32 bits (i386) ou Intel 64 bits (x86_64), o código está disponível para ambas as arquiteturas no arquivo pois é universal. Na prática, nem sempre é possível construir diretamente um binário universal apenas passando os parâmetros -arch.
Uma forma alternativa de gerar um binário universal está em compilar o fonte em diversas arquiteturas separadas (em arquivos) e depois juntar as partes em um único arquivo executável, através do utilitário lipo.
$ gcc -arch i386 hello.c -o hello-i386 $ gcc -arch x86_64 hello.c -o hello-x86_64 $ lipo -create -arch i386 hello-i386 -arch x86_64 hello-x86_64 -o hello $ lipo -i hello Architectures in the fat file: hello are: i386 x86_64
Em projetos de software da vida real, deve modificar as variáveis CFLAGS, CXXFLAGS e LDFLAGS para incluir as opções de arch, algo como CFLAGS="-arch i386 -arch x86_64 -Os" LDFLAGS="-arch i386 -arch x86_64" para obter um binário universal.
Muitas vezes os projetos que utilizam autotools (o popular configure) disponibilizam uma opção para habitilar a construção de universal, algo como: --enable-universal, --enable-nextstep ou enable-darwin, que nada mais faz do que configurar as variávies FLAGS apresentadas.