Depois de descobrir que vários comandos comuns (como read
) são na verdade internos do Bash (e ao executá-los no prompt, na verdade, estou executando um script de shell de duas linhas que apenas encaminha para o interno), eu estava procurando ver se o mesmo é verdadeiro para true
e false
.
Bem, eles são definitivamente binários.
sh-4.2$ which true
/usr/bin/true
sh-4.2$ which false
/usr/bin/false
sh-4.2$ file /usr/bin/true
/usr/bin/true: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=2697339d3c19235
06e10af65aa3120b12295277e, stripped
sh-4.2$ file /usr/bin/false
/usr/bin/false: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=b160fa513fcc13
537d7293f05e40444fe5843640, stripped
sh-4.2$
No entanto, o que mais me surpreendeu foi o tamanho. Eu esperava que eles tivessem apenas alguns bytes cada, pois true
é basicamente apenas exit 0
e false
é exit 1
.
sh-4.2$ true
sh-4.2$ echo $?
0
sh-4.2$ false
sh-4.2$ echo $?
1
sh-4.2$
No entanto, descobri, para minha surpresa, que ambos os arquivos têm mais de 28 KB de tamanho.
sh-4.2$ stat /usr/bin/true
File: '/usr/bin/true'
Size: 28920 Blocks: 64 IO Block: 4096 regular file
Device: fd2ch/64812d Inode: 530320 Links: 1
Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2018-01-25 19:46:32.703463708 +0000
Modify: 2016-06-30 09:44:27.000000000 +0100
Change: 2017-12-22 09:43:17.447563336 +0000
Birth: -
sh-4.2$ stat /usr/bin/false
File: '/usr/bin/false'
Size: 28920 Blocks: 64 IO Block: 4096 regular file
Device: fd2ch/64812d Inode: 530697 Links: 1
Access: (0755/-rwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2018-01-25 20:06:27.210764704 +0000
Modify: 2016-06-30 09:44:27.000000000 +0100
Change: 2017-12-22 09:43:18.148561245 +0000
Birth: -
sh-4.2$
Então, minha pergunta é: por que eles são tão grandes? O que há no executável além do código de retorno?
PS: Estou usando o RHEL 7.4
No passado,
/bin/true
e/bin/false
no shell, havia realmente scripts.Por exemplo, em um PDP/11 Unix System 7:
Atualmente, pelo menos no
bash
, os comandostrue
efalse
são implementados como comandos internos do shell. Portanto, nenhum arquivo binário executável é invocado por padrão, tanto ao usar as diretivasfalse
e na linha de comando quanto dentro de scripts de shell.true
bash
Da
bash
fontebuiltins/mkbuiltins.c
,:Também pelos comentários do @meuh:
Portanto, pode-se dizer com alto grau de certeza que os arquivos
true
efalse
executáveis existem principalmente para serem chamados de outros programas .A partir de agora, a resposta se concentrará no
/bin/true
binário docoreutils
pacote em Debian 9/64 bits. (/usr/bin/true
rodando RedHat. RedHat e Debian usam ambos ocoreutils
pacote, analisamos a versão compilada deste último tendo mais em mãos).Como pode ser visto no arquivo fonte
false.c
,/bin/false
é compilado com (quase) o mesmo código fonte que/bin/true
, apenas retornando EXIT_FAILURE (1), então esta resposta pode ser aplicada para ambos os binários.Como também pode ser confirmado por ambos os executáveis com o mesmo tamanho:
Infelizmente, a pergunta direta para a resposta
why are true and false so large?
poderia ser, porque não há mais razões tão prementes para se preocupar com seu desempenho superior. Eles não são essenciais para obash
desempenho, não sendo mais usados porbash
(scripts).Comentários semelhantes se aplicam ao seu tamanho, 26 KB para o tipo de hardware que temos hoje em dia é insignificante. O espaço não é mais premium para o servidor/desktop típico, e eles nem se preocupam mais em usar o mesmo binário para
false
etrue
, pois ele é implantado apenas duas vezes em distribuições usandocoreutils
.Focalizando, porém, no verdadeiro espírito da questão, por que algo que deveria ser tão simples e pequeno, se torna tão grande?
A distribuição real das seções de
/bin/true
é como mostram esses gráficos; o código principal + dados equivale a aproximadamente 3 KB de um binário de 26 KB, o que equivale a 12% do tamanho de/bin/true
.O
true
utilitário obteve de fato mais código cruft ao longo dos anos, principalmente o suporte padrão para--version
e--help
.No entanto, essa não é a (única) principal justificativa para ser tão grande, mas sim, ao mesmo tempo em que é vinculado dinamicamente (usando bibliotecas compartilhadas), também possui parte de uma biblioteca genérica comumente usada por
coreutils
binários vinculados como uma biblioteca estática. A metada para a construção de umelf
arquivo executável também representa uma parte significativa do binário, sendo um arquivo relativamente pequeno para os padrões atuais.O restante da resposta é para explicar como construímos os gráficos a seguir detalhando a composição do
/bin/true
arquivo binário executável e como chegamos a essa conclusão.Como diz @Maks, o binário foi compilado de C; de acordo com meu comentário também, também está confirmado que é do coreutils. Estamos apontando diretamente para o(s) autor(es) git https://github.com/wertarbyte/coreutils/blob/master/src/true.c , em vez do gnu git como @Maks (mesmas fontes, repositórios diferentes - este repositório foi selecionado, pois possui o código-fonte completo das
coreutils
bibliotecas)Podemos ver os vários blocos de construção do
/bin/true
binário aqui (Debian 9 - 64 bits decoreutils
):Daqueles:
Dos 24KB, cerca de 1KB é para fixar as 58 funções externas.
Isso ainda deixa cerca de 23 KB para o restante do código. Mostraremos abaixo que o arquivo principal real - código main()+usage() tem cerca de 1KB compilado e explicamos para que os outros 22KB são usados.
Aprofundando o binário com
readelf -S true
, podemos ver que, embora o binário seja de 26159 bytes, o código compilado real é de 13017 bytes e o restante é um código de dados/inicialização variado.No entanto,
true.c
não é toda a história e 13 KB parece muito excessivo se fosse apenas esse arquivo; podemos ver as funções chamadasmain()
que não estão listadas nas funções externas vistas no elf comobjdump -T true
; funções que estão presentes em:Essas funções extras não vinculadas externamente
main()
são:Portanto, minha primeira suspeita estava parcialmente correta, embora a biblioteca esteja usando bibliotecas dinâmicas, o
/bin/true
binário é grande *porque possui algumas bibliotecas estáticas incluídas* (mas essa não é a única causa).A compilação do código C geralmente não é tão ineficiente por ter esse espaço não contabilizado, portanto, minha suspeita inicial de que algo estava errado.
O espaço extra, quase 90% do tamanho do binário, é de fato bibliotecas extras/metadados elf.
Ao usar o Hopper para desmontar/descompilar o binário para entender onde estão as funções, pode-se ver que o código binário compilado da função true.c/usage() é na verdade 833 bytes e da função true.c/main() é 225 bytes, que é aproximadamente um pouco menos de 1 KB. A lógica das funções de versão, que está escondida nas bibliotecas estáticas, é de cerca de 1KB.
O main()+usage()+version()+strings+vars compilado real está usando apenas cerca de 3KB a 3,5KB.
É realmente irônico que essas pequenas e humildes utilidades tenham se tornado maiores pelas razões explicadas acima.
pergunta relacionada: Entendendo o que um binário do Linux está fazendo
true.c
main () com as chamadas de função ofensivas:O tamanho decimal das várias seções do binário:
Saída de
readelf -S true
Saída de
objdump -T true
(funções externas vinculadas dinamicamente em tempo de execução)A implementação provavelmente vem do GNU coreutils. Esses binários são compilados de C; nenhum esforço particular foi feito para torná-los menores do que são por padrão.
Você pode tentar compilar a implementação trivial de
true
si mesmo e notará que já tem alguns KB de tamanho. Por exemplo, no meu sistema:Claro, seus binários são ainda maiores. Isso porque eles também suportam argumentos de linha de comando. Tente executar
/usr/bin/true --help
ou/usr/bin/true --version
.Além dos dados de string, o binário inclui lógica para analisar sinalizadores de linha de comando, etc. Isso soma cerca de 20 KB de código, aparentemente.
Para referência, você pode encontrar o código-fonte aqui: http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/true.c
Stripping them down to core functionality and writing in assembler yields far smaller binaries.
Original true/false binaries are written in C, which by its nature pulls in various library + symbol references. If you run
readelf -a /bin/true
this is quite noticeable.352 bytes for a stripped ELF static executable (with room to save a couple bytes by optimizing the asm for code-size).
Or, with a bit of a nasty/ingenious approach (kudos to stalkr), create your own ELF headers, getting it down to
132127 bytes. We're entering Code Golf territory here.Pretty big on my Ubuntu 16.04 too. exactly the same size? What makes them so big?
(excerpt:)
Ah, there is help for true and false, so let's try it:
Nothing. Ah, there was this other line:
So on my system, it's /bin/true, not /usr/bin/true
So there is help, there is version information, binding to a library for internationalization. This explains much of the size, and the shell uses its optimized command anyway and most of the time.
The accepted answer by Rui F Ribeiro gives a lot of good information, but it's missing some and I feel like it leaves the reader with a misleading impression that the code size is small relative to "ELF overhead" and similar.
Static linking is at object file granularity (or even finer with
--gc-sections
), so it doesn't make sense to talk about a static library being "linked in"; the only part linked in is what's used, and the code size here is code that's actually (gratuitously) used by the coreutils version oftrue
. It does not make sense to count it separately from what appears intrue.c
.The ELF metadata is pretty much entirely tables necessary for dynamic linking, and is nowhere near 90% of the size. These are the lines from
size -A
output relevant to it:for a total of around 5.5k, or an average of about 100 bytes per dynamic symbol (not quite fair because some aren't on a per-symbol basis, but it's a somewhat meaningful figure).
One large contributor to size that Rui's answer didn't cover is:
This 3.5k is DWARF unwind tables for C++ exception handling, introspective backtrace, etc. They're completely useless for
true
, but included by GCC policy in all output unless you explicitly omit them with a very verbosely-named option-fno-asynchronous-unwind-tables
. The motivations behind this are explained in an answer by me on Stack Overflow.So I would describe the final breakdown as:
Em particular, é notável que, se a quantidade de código necessária de bibliotecas com links dinâmicos fosse suficientemente pequena, o link estático poderia ser menor do que o link dinâmico.