Пользователь с Reddit под ником mathisweirdaf поделился интересными наблюдениями:
$ ls -lh /usr/bin/{test,[}-rwxr-xr-x 1 root root 59K Sep 5 2019 '/usr/bin/['-rwxr-xr-x 1 root root 55K Sep 5 2019 /usr/bin/test
[
и test
должны быть псевдонимами друг
друга, и все же между исполняющими их файлами из GNU
coreutils наблюдается разница в 4Кб. Почему?Во-первых, для всех, кого это удивило: да, существует
/usr/bin/[
. По этой теме у меня есть
отдельная статья (англ.), но я все же коротко поясню:Когда вы пишите
if [ -e /etc/passwd ]; then ..
эта
скобка выступает не синтаксисом оболочки, а просто стандартной
командой с забавным именем. Обычно она встроена в оболочку, но
иногда может реализовываться через /usr/bin/[
. Это
объясняет многое из ее загадочного поведения, например, почему она
чувствительна к пробелам: [1=2]
оказывается не более
валидно, чем ls-l/tmp
.Тем не менее откуда возникает разница в размере? Можно сравнить вывод objdump, чтобы увидеть, куда помещаются данные. Вот выдержка из
objdump -h /usr/bin/[
:
size offset15 .text 00006e82 0000000000002640 0000000000002640 00002640 2**416 .fini 0000000d 00000000000094c4 00000000000094c4 000094c4 2**217 .rodata 00001e4c 000000000000a000 000000000000a000 0000a000 2**5
А вот objdump
-h /usr/bin/test
:
15 .text 000068a2 0000000000002640 0000000000002640 00002640 2**416 .fini 0000000d 0000000000008ee4 0000000000008ee4 00008ee4 2**217 .rodata 00001aec 0000000000009000 0000000000009000 00009000 2**5
Здесь мы видим, что сегмент
.text
(скомпилированный
исполняемый код) на 1504 байта больше, в то время как
.rodata
(постоянные значения и строки) больше на 864
байта.Суть в том, что увеличенный размер сегмента
.text
вынуждает его перемещаться из 8000
в
9000
, пересекая границу размера страницы
0х1000
(4096) и, следовательно, смещая все другие
сегменты на 4096 байтов. Именно эту разницу в размере мы и
наблюдаем.Единственное номинальное отличие между
[
и
test
в том, что [
требует наличия
]
в качестве заключительного аргумента. Проверка этого
потребовала бы минимальное количество кода, так зачем все же
используются те самые ~1500 байтов?Поскольку сложно просматривать отделенные исполнительные файлы, я создал собственную копию coreutils и сравнил список функций в каждом:
$ diff -u <(nm -S --defined-only src/[ | cut -d ' ' -f 2-) <(nm -S --defined-only src/test | cut -d ' ' -f 2-)--- /dev/fd/63 2021-02-02 20:21:35.337942508 -0800+++ /dev/fd/62 2021-02-02 20:21:35.341942491 -0800@@ -37,7 +37,6 @@ D __dso_handle d _DYNAMIC D _edata-0000000000000099 T emit_bug_reporting_address B _end 0000000000000004 D exit_failure 0000000000000008 b file_name@@ -63,7 +62,7 @@ 0000000000000022 T locale_charset 0000000000000014 T __lstat 0000000000000014 t lstat-0000000000000188 T main+00000000000000d1 T main 000000000000000b T make_timespec 0000000000000004 d nslots 0000000000000022 t one_argument@@ -142,16 +141,10 @@ 0000000000000032 T umaxtostr 0000000000000013 t unary_advance 00000000000004e5 t unary_operator-00000000000003d2 T usage+0000000000000428 T usage 0000000000000d2d T vasnprintf 0000000000000013 T verror 00000000000000ae T verror_at_line-0000000000000008 D Version-00000000000000ab T version_etc-0000000000000018 T version_etc_ar-000000000000042b T version_etc_arn-000000000000002f R version_etc_copyright-000000000000007a T version_etc_va 000000000000001c r wide_null_string.2840 0000000000000078 T x2nrealloc 000000000000000e T x2realloc
Главные участники это функции
version_etc*
. Что они
делают?Давайте посмотрим:
/* The three functions below display the --version information the standard way [...]
Это 260 строк развернутых, интернационализированных, условных способов форматирования данных, которые составляют вывод
--version
. Все вместе они занимают около bc
<<< "ibase=16; 7A+2F+42B+18+AB+8+99" = 1592
байтов
.Что это значит? Все просто. Дополнительные 4Кб уходят вот на что:
$ /usr/bin/[ --version[ (GNU coreutils) 8.30Copyright (C) 2018 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Written by Kevin Braunsdorf and Matthew Bradburn.
[ --version
недостает заключительной ]
,
поэтому вызов оказывается недействительным, и результат
определяется реализацией. GNU спокойно позволяет вывести информацию
о версии.Тем временем
/usr/bin/test --version
оказывается
действительным вызовом, и POSIX предписывает, чтобы она возвращала
успех, когда первый параметр (--version
) является не
пустой строкой.Эта разница даже упоминается в документации:
Примечание: [ отвечает за опции --help и --version, а test нет.test рассматривает каждую из них просто как непустую строку.
Загадка разгадана!
(Задачка: каковы будут последствия, если вопреки POSIX
test
будет поддерживать --help
и
--version
?)