Pull to refresh

Comments 74

Я бы сказал этот трюк допустим для ядра, которое может контролировать своё адресное пространство, но вот для приложений он опасен.

Единственное, что можно извлечь в userland — это проверку на IS_NULL (от 0 до N), где N — какое-то условное значение, при условии, что программа чётко знает, что в этом промежутке нет области данных (с любыми правами доступа). Такую гарантию можно дать только зная скрипт линковщика, и то, если не допустимы перемещения.

Какой смысл? Очень часто бывает, что возвращают адрес смещения поля в структуре, что не проходит проверку на ноль, если сам указатель структуры был нулевой и поле не первое. Обращения по таким адресам на некоторых архитектурах вообще вызывает машинные прерывания с интересными последствиями.
Хоть и истина есть в ваших рассуждениях, но вероятность того, что наш процесс будет занимать области памяти (в виртуальном адресном пространстве, если есть MMU) 0…16 и 2^32-1024…2^32-1 весьма мала, согласитесь.
Соглашусь, но вы ведь не программируете бизнес логику полагаясь на теорию вероятностей? Оптимизация — да, но не контроль ошибок.

При этом в любом случае надо создавать отдельный тип, который ЧЁТКО указывает, что вместо указателя может быть возращено мракобесие.

Пример:
struct MyType;
typedef struct MyType *MyTypeOrError;


Ну, а лучше вообще union:

union MyTypeOrError {
    struct MyType *my_type;
    ErrorCode error_code;
};

Но как в случае этого union'а определить, какой из результатов актуален?
Вот тогда и начинаем мракобесие с макросами.

Если же С++, то эту кухню можно спрятать в member function union'а, поскольку без выполнения определённых условий union остаётся POD-типом данных. Для всего прочего притянуть template и поставить на поток…

Фантазия может улететь далеко, но в С++ для решения той же задачи есть exceptions, а в C — thread safe errno.
Поэтому, я принципиально против таких интерфейсов.
Это было сказано в контексте приложений… К сожалению, стандартная библиотека C и интерфейс POSIX есть какой есть. Их можно любить, можно ненавидеть.
Торвальдс о тяжёлом наследии *nix.
Можно в принципе не полагаться на теорию вероятностей, а посмотреть, как происходит распределение сегментов памяти в программе. По крайней мере NULL — это невалидный указатель. Далее исходим из того, что память ядро выравнивает по 4 килобайта, а также из того, что по младшим адресам всегда помещаются сегменты кода и данных. То есть ядро (и тем более malloc) не будет размещать какие-либо данные как минимум в диапазоне адресов 0-4095.

Ну и /proc/xxxx/maps в помощь. Минус в том, что за все операционные системы ручаться нельзя, да и непонятно, что там на встраиваемых системах. На 64-разрядной машине виртуальная область памяти вообще содержит массу «дырок»:
Скрытый текст
$ sudo pmap `/usr/bin/pgrep X`
7287:   /etc/X11/X :0 -auth /var/run/lightdm/root/:0 -nolisten tcp vt1 -novtswitch
0000000000400000   2032K r-x--  /usr/bin/Xorg
00000000007fb000      8K r----  /usr/bin/Xorg
00000000007fd000     48K rw---  /usr/bin/Xorg
0000000000809000     64K rw---    [ anon ]
0000000000dd3000  87424K rw---    [ anon ]
00007f6f76c5f000   4096K rw-s-    [ shmid=0x3aa00022 ]
00007f6f770df000    512K rw-s-    [ shmid=0x3ab4001b ]
00007f6f7715f000    384K rw-s-    [ shmid=0x3a9b001e ]
00007f6f771bf000   1072K rw-s-    [ shmid=0x3a9a801f ]
00007f6f772cb000    384K rw-s-    [ shmid=0x3a99001d ]
00007f6f7732b000   8476K rw-s-    [ shmid=0x3a97001a ]
00007f6f77b72000    384K rw-s-    [ shmid=0x3a8a000d ]
00007f6f77bf2000    384K rw-s-    [ shmid=0x3a97801c ]
00007f6f77c52000    384K rw-s-    [ shmid=0x3a900019 ]
00007f6f77cb2000    384K rw-s-    [ shmid=0x3a830018 ]
00007f6f77d12000   4096K rw-s-    [ shmid=0x3a828017 ]
00007f6f78112000    512K rw-s-    [ shmid=0x3a820016 ]
00007f6f78192000    384K rw-s-    [ shmid=0x3a808015 ]
00007f6f781f2000    384K rw-s-    [ shmid=0x3a800013 ]
00007f6f78252000    384K rw-s-    [ shmid=0x3a7f000e ]
00007f6f782b2000    512K rw-s-    [ shmid=0x3a7b0012 ]
00007f6f78332000    384K rw-s-    [ shmid=0x3a798010 ]
00007f6f783b2000    384K rw-s-    [ shmid=0x3a8e8014 ]
00007f6f78412000    384K rw-s-    [ shmid=0x3a7d800b ]
00007f6f78472000    384K rw-s-    [ shmid=0x3a768009 ]
00007f6f784d2000    512K rw-s-    [ shmid=0x3a75800c ]
00007f6f78572000    512K rw-s-    [ shmid=0x3ab48021 ]
00007f6f785f2000    384K rw-s-    [ shmid=0x3a94000f ]
00007f6f78652000    384K rw-s-    [ shmid=0x3a73800a ]
00007f6f786b2000    384K rw-s-    [ shmid=0x3a728008 ]
00007f6f78712000    512K rw-s-    [ shmid=0x3a720007 ]
00007f6f78792000   9004K rw---    [ anon ]
00007f6f7905d000    384K rw-s-    [ shmid=0x3a6f8006 ]
00007f6f790bd000  18008K rw---    [ anon ]
00007f6f7a253000    384K rw-s-    [ shmid=0x3a6c8002 ]
00007f6f7a2b3000     48K r-x--  /usr/lib64/libnss_files-2.18.so
00007f6f7a2bf000   2044K -----  /usr/lib64/libnss_files-2.18.so
00007f6f7a4be000      4K r----  /usr/lib64/libnss_files-2.18.so
00007f6f7a4bf000      4K rw---  /usr/lib64/libnss_files-2.18.so
00007f6f7a4e1000     48K r-x--  /usr/lib64/xorg/modules/input/evdev_drv.so
00007f6f7a4ed000   2044K -----  /usr/lib64/xorg/modules/input/evdev_drv.so
00007f6f7a6ec000      4K r----  /usr/lib64/xorg/modules/input/evdev_drv.so
00007f6f7a6ed000      4K rw---  /usr/lib64/xorg/modules/input/evdev_drv.so
00007f6f7a6fe000    788K rw-s-  /dev/nvidia0
00007f6f7a7c3000   8192K rw-s-  /dev/nvidia0
00007f6f7afc3000    188K r-x--  /usr/lib64/xorg/modules/libwfb.so
00007f6f7aff2000   2044K -----  /usr/lib64/xorg/modules/libwfb.so
00007f6f7b1f1000      4K r----  /usr/lib64/xorg/modules/libwfb.so
00007f6f7b1f2000      4K rw---  /usr/lib64/xorg/modules/libwfb.so
00007f6f7b1f3000    144K r-x--  /usr/lib64/xorg/modules/libfb.so
00007f6f7b217000   2044K -----  /usr/lib64/xorg/modules/libfb.so
00007f6f7b416000      4K r----  /usr/lib64/xorg/modules/libfb.so
00007f6f7b417000      4K rw---  /usr/lib64/xorg/modules/libfb.so
00007f6f7b418000   7156K r-x--  /usr/lib64/nvidia-current/xorg/nvidia_drv.so
00007f6f7bb15000   2048K -----  /usr/lib64/nvidia-current/xorg/nvidia_drv.so
00007f6f7bd15000    784K rw---  /usr/lib64/nvidia-current/xorg/nvidia_drv.so
00007f6f7bdd9000    444K rw---    [ anon ]
00007f6f7be48000  28064K r-x--  /usr/lib64/nvidia-current/libnvidia-glcore.so.331.113
00007f6f7d9b0000   2044K -----  /usr/lib64/nvidia-current/libnvidia-glcore.so.331.113
00007f6f7dbaf000  10796K rwx--  /usr/lib64/nvidia-current/libnvidia-glcore.so.331.113
00007f6f7e63a000    116K rwx--    [ anon ]
00007f6f7e657000     12K r-x--  /usr/lib64/nvidia-current/tls/libnvidia-tls.so.331.113
00007f6f7e65a000   2044K -----  /usr/lib64/nvidia-current/tls/libnvidia-tls.so.331.113
00007f6f7e859000      4K rw---  /usr/lib64/nvidia-current/tls/libnvidia-tls.so.331.113
00007f6f7e85a000   9480K r-x--  /usr/lib64/nvidia-current/xorg/libglx.so.331.113
00007f6f7f19c000   2048K -----  /usr/lib64/nvidia-current/xorg/libglx.so.331.113
00007f6f7f39c000   2404K rwx--  /usr/lib64/nvidia-current/xorg/libglx.so.331.113
00007f6f7f5f5000     16K rwx--    [ anon ]
00007f6f7f5f9000     16K r-x--  /usr/lib64/xorg/modules/drivers/v4l_drv.so
00007f6f7f5fd000   2044K -----  /usr/lib64/xorg/modules/drivers/v4l_drv.so
00007f6f7f7fc000      4K r----  /usr/lib64/xorg/modules/drivers/v4l_drv.so
00007f6f7f7fd000      4K rw---  /usr/lib64/xorg/modules/drivers/v4l_drv.so
00007f6f7f7fe000     84K r-x--  /usr/lib64/libgcc_s-4.8.2.so.1
00007f6f7f813000   2044K -----  /usr/lib64/libgcc_s-4.8.2.so.1
00007f6f7fa12000      4K r----  /usr/lib64/libgcc_s-4.8.2.so.1
00007f6f7fa13000      4K rw---  /usr/lib64/libgcc_s-4.8.2.so.1
00007f6f7fa14000    404K r-x--  /usr/lib64/libpcre.so.1.2.1
00007f6f7fa79000   2044K -----  /usr/lib64/libpcre.so.1.2.1
00007f6f7fc78000      4K r----  /usr/lib64/libpcre.so.1.2.1
00007f6f7fc79000      4K rw---  /usr/lib64/libpcre.so.1.2.1
00007f6f7fc7a000    128K r-x--  /usr/lib64/libgraphite2.so.3.0.1
00007f6f7fc9a000   2044K -----  /usr/lib64/libgraphite2.so.3.0.1
00007f6f7fe99000      8K r----  /usr/lib64/libgraphite2.so.3.0.1
00007f6f7fe9b000      4K rw---  /usr/lib64/libgraphite2.so.3.0.1
00007f6f7fe9c000   1016K r-x--  /usr/lib64/libglib-2.0.so.0.3800.2
00007f6f7ff9a000   2044K -----  /usr/lib64/libglib-2.0.so.0.3800.2
00007f6f80199000      4K r----  /usr/lib64/libglib-2.0.so.0.3800.2
00007f6f8019a000      4K rw---  /usr/lib64/libglib-2.0.so.0.3800.2
00007f6f8019b000      4K rw---    [ anon ]
00007f6f8019c000    324K r-x--  /usr/lib64/libharfbuzz.so.0.922.0
00007f6f801ed000   2048K -----  /usr/lib64/libharfbuzz.so.0.922.0
00007f6f803ed000      4K r----  /usr/lib64/libharfbuzz.so.0.922.0
00007f6f803ee000      4K rw---  /usr/lib64/libharfbuzz.so.0.922.0
00007f6f803ef000    376K r-x--  /usr/lib64/libpng16.so.16.16.0
00007f6f8044d000   2044K -----  /usr/lib64/libpng16.so.16.16.0
00007f6f8064c000      4K r----  /usr/lib64/libpng16.so.16.16.0
00007f6f8064d000      4K rw---  /usr/lib64/libpng16.so.16.16.0
00007f6f8064e000     24K r-x--  /usr/lib64/libfontenc.so.1.0.0
00007f6f80654000   2044K -----  /usr/lib64/libfontenc.so.1.0.0
00007f6f80853000      4K r----  /usr/lib64/libfontenc.so.1.0.0
00007f6f80854000      4K rw---  /usr/lib64/libfontenc.so.1.0.0
00007f6f80855000      4K rw---    [ anon ]
00007f6f80856000     60K r-x--  /usr/lib64/libbz2.so.1.0.6
00007f6f80865000   2044K -----  /usr/lib64/libbz2.so.1.0.6
00007f6f80a64000      4K r----  /usr/lib64/libbz2.so.1.0.6
00007f6f80a65000      4K rw---  /usr/lib64/libbz2.so.1.0.6
00007f6f80a66000    604K r-x--  /usr/lib64/libfreetype.so.6.11.3
00007f6f80afd000   2044K -----  /usr/lib64/libfreetype.so.6.11.3
00007f6f80cfc000     24K r----  /usr/lib64/libfreetype.so.6.11.3
00007f6f80d02000      4K rw---  /usr/lib64/libfreetype.so.6.11.3
00007f6f80d03000     96K r-x--  /usr/lib64/libz.so.1.2.8
00007f6f80d1b000   2048K -----  /usr/lib64/libz.so.1.2.8
00007f6f80f1b000      4K r----  /usr/lib64/libz.so.1.2.8
00007f6f80f1c000      4K rw---  /usr/lib64/libz.so.1.2.8
00007f6f80f1d000     28K r-x--  /usr/lib64/librt-2.18.so
00007f6f80f24000   2044K -----  /usr/lib64/librt-2.18.so
00007f6f81123000      4K r----  /usr/lib64/librt-2.18.so
00007f6f81124000      4K rw---  /usr/lib64/librt-2.18.so
00007f6f81125000   1712K r-x--  /usr/lib64/libc-2.18.so
00007f6f812d1000   2044K -----  /usr/lib64/libc-2.18.so
00007f6f814d0000     16K r----  /usr/lib64/libc-2.18.so
00007f6f814d4000      8K rw---  /usr/lib64/libc-2.18.so
00007f6f814d6000     16K rw---    [ anon ]
00007f6f814da000   1032K r-x--  /usr/lib64/libm-2.18.so
00007f6f815dc000   2044K -----  /usr/lib64/libm-2.18.so
00007f6f817db000      4K r----  /usr/lib64/libm-2.18.so
00007f6f817dc000      4K rw---  /usr/lib64/libm-2.18.so
00007f6f817dd000     20K r-x--  /usr/lib64/libXdmcp.so.6.0.0
00007f6f817e2000   2044K -----  /usr/lib64/libXdmcp.so.6.0.0
00007f6f819e1000      4K r----  /usr/lib64/libXdmcp.so.6.0.0
00007f6f819e2000      4K rw---  /usr/lib64/libXdmcp.so.6.0.0
00007f6f819e3000      8K r-x--  /usr/lib64/libXau.so.6.0.0
00007f6f819e5000   2048K -----  /usr/lib64/libXau.so.6.0.0
00007f6f81be5000      4K r----  /usr/lib64/libXau.so.6.0.0
00007f6f81be6000      4K rw---  /usr/lib64/libXau.so.6.0.0
00007f6f81be7000    236K r-x--  /usr/lib64/libXfont.so.1.4.1
00007f6f81c22000   2048K -----  /usr/lib64/libXfont.so.1.4.1
00007f6f81e22000      4K r----  /usr/lib64/libXfont.so.1.4.1
00007f6f81e23000      8K rw---  /usr/lib64/libXfont.so.1.4.1
00007f6f81e25000    652K r-x--  /usr/lib64/libpixman-1.so.0.32.4
00007f6f81ec8000   2048K -----  /usr/lib64/libpixman-1.so.0.32.4
00007f6f820c8000     28K r----  /usr/lib64/libpixman-1.so.0.32.4
00007f6f820cf000      4K rw---  /usr/lib64/libpixman-1.so.0.32.4
00007f6f820d0000     44K r-x--  /usr/lib64/libdrm.so.2.4.0
00007f6f820db000   2044K -----  /usr/lib64/libdrm.so.2.4.0
00007f6f822da000      4K r----  /usr/lib64/libdrm.so.2.4.0
00007f6f822db000      4K rw---  /usr/lib64/libdrm.so.2.4.0
00007f6f822dc000     96K r-x--  /usr/lib64/libpthread-2.18.so
00007f6f822f4000   2044K -----  /usr/lib64/libpthread-2.18.so
00007f6f824f3000      4K r----  /usr/lib64/libpthread-2.18.so
00007f6f824f4000      4K rw---  /usr/lib64/libpthread-2.18.so
00007f6f824f5000     16K rw---    [ anon ]
00007f6f824f9000     32K r-x--  /usr/lib64/libpciaccess.so.0.11.1
00007f6f82501000   2044K -----  /usr/lib64/libpciaccess.so.0.11.1
00007f6f82700000      4K r----  /usr/lib64/libpciaccess.so.0.11.1
00007f6f82701000      4K rw---  /usr/lib64/libpciaccess.so.0.11.1
00007f6f82702000     12K r-x--  /usr/lib64/libdl-2.18.so
00007f6f82705000   2044K -----  /usr/lib64/libdl-2.18.so
00007f6f82904000      4K r----  /usr/lib64/libdl-2.18.so
00007f6f82905000      4K rw---  /usr/lib64/libdl-2.18.so
00007f6f82906000   1884K r-x--  /usr/lib64/libcrypto.so.1.0.0
00007f6f82add000   2048K -----  /usr/lib64/libcrypto.so.1.0.0
00007f6f82cdd000    104K r----  /usr/lib64/libcrypto.so.1.0.0
00007f6f82cf7000     44K rw---  /usr/lib64/libcrypto.so.1.0.0
00007f6f82d02000     16K rw---    [ anon ]
00007f6f82d06000     64K r-x--  /usr/lib64/libudev.so.1.4.0
00007f6f82d16000   2048K -----  /usr/lib64/libudev.so.1.4.0
00007f6f82f16000      4K r----  /usr/lib64/libudev.so.1.4.0
00007f6f82f17000      4K rw---  /usr/lib64/libudev.so.1.4.0
00007f6f82f18000    120K r-x--  /usr/lib64/ld-2.18.so
00007f6f82f63000      4K rw-s-  /dev/nvidia0
00007f6f82f64000      4K rw-s-  /dev/nvidia0
00007f6f82f65000      4K rw-s-  /dev/nvidia0
00007f6f82f66000      4K rw-s-  /dev/nvidia0
00007f6f82f67000      4K rw-s-  /dev/nvidia0
00007f6f82f68000      4K rw-s-  /dev/nvidia0
00007f6f82f69000      4K rw-s-  /dev/nvidia0
00007f6f82f6a000      4K rw-s-  /dev/nvidia0
00007f6f82f6b000      4K rw-s-  /dev/nvidia0
00007f6f82f6c000      4K rw-s-  /dev/nvidia0
00007f6f82f6d000    384K rw-s-    [ shmid=0x3a6f0005 ]
00007f6f82fcd000    384K rw-s-    [ shmid=0x3a6e8004 ]
00007f6f8302d000    384K rw-s-    [ shmid=0x3a6e0003 ]
00007f6f8308d000     64K rw-s-  /dev/nvidia0
00007f6f8309d000    128K rw-s-  /dev/nvidia0
00007f6f830bd000    348K rw---    [ anon ]
00007f6f83114000      4K rw-s-  /dev/nvidia0
00007f6f83115000      4K rw-s-  /dev/nvidia0
00007f6f83116000      4K rw-s-  /dev/nvidia0
00007f6f83117000      4K rw-s-  /dev/nvidia0
00007f6f83118000      4K rw-s-  /dev/nvidia0
00007f6f83119000      4K rw-s-  /dev/nvidia0
00007f6f8311a000      4K rw-s-  /dev/nvidia0
00007f6f8311b000      4K rw-s-  /dev/nvidia0
00007f6f8311c000      4K rw-s-  /dev/nvidia0
00007f6f8311d000      4K rw-s-  /dev/nvidia0
00007f6f8311e000      4K rw-s-  /dev/nvidia0
00007f6f8311f000      4K rw-s-  /dev/nvidia0
00007f6f83120000      4K rw-s-  /dev/nvidia0
00007f6f83121000     68K rw-s-  /dev/nvidia0
00007f6f83132000      4K rw-s-  /dev/nvidia0
00007f6f83133000      4K rw-s-  /dev/nvidia0
00007f6f83134000      8K rw---    [ anon ]
00007f6f83136000      4K r----  /usr/lib64/ld-2.18.so
00007f6f83137000      4K rw---  /usr/lib64/ld-2.18.so
00007f6f83138000      4K rw---    [ anon ]
00007fff52412000    356K rw---    [ stack ]
00007fff524b9000      8K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
Оставим невесть куда утекающие 87424K «кучи» на совести разработчиков, и обратим внимание, что ядро оставляет приличные области адресного пространства как «сверху», так и «снизу».
Тут есть тонкости. Скажем на 32-битных OS'ях вам никогда не выделят ничего в диапазоне 2^32-1024…2^32-1 потому что там ядро OS живёт. А вот на системе с 64-битным ядром — такое уже может случиться. Нужно просто постараться «забрать себе» эту страничку. Как можно раньше после запуска программы. Если её не дали (32-битная OS) — то и никому её не дадут, если дали — то таки дали (и тоже больше уже никому не дадут). В идеале это вообще loader должен бы сделать, но его мы не можем контролировать (хотя если хочется 200% надёжности можно сделать как в nacl_bootsrap.
Какая к бесу теория вероятностй? При старте программы пытаетесь сходу аллоцировать первую и последнюю страницы. И всё. Получилось — они ваши, больше никаких объектов там не будет, не получилось — значит они не ваши (так на всех современных 32-битных OS'ах будет), но это же и значит, что больше вам там ничего не дадут. Всё. Делов-то.
Это вы сейчас о чем?
О user-программах, или о ядре?

Если о ядре. Допустим даже способ на х86 рабочий. Но кто гарантирует, что в других архитектурах оно не ляжет неблагоприятно?

Если о пользовательских программах — все сложней, чем вы написали. Даже если при старте захватить страницу не вышло, кто-нибудь эту страницу может когда-нибудь отпустить, она станет свободной. Потом ваш процесс запросит память — отдастся эта страница. А вы полагаетесь на то, что она вам не отдастся никогда.
Даже если при старте захватить страницу не вышло, кто-нибудь эту страницу может когда-нибудь отпустить, она станет свободной.
Угу. А ещё нас всех могут захватить инопланетяне. Нет, есть, конечно, шанс, что когда вы применяете эту технологию не в программе, а в библиотеке, что ядро выделит кому-то вот прямо-таки последнюю доступную страничку, а потом этот кто-то эту страничку отпустит, но это какие-то ужасы из разряда попадания кометой прямо вам в лоб. Даже в системах, где доступны все 4GiB ядро вряд ли будет так запросто эту страницу выделять без крайней нужды (не тот регион), так что тут мы боремся уже не случайным распределением памяти, а с какими-то вредителями, которые вас пытаются обмануть. Причём эти вредители живут прямо в адресном пространстве вашего приложение и, в общем-то, могут докучать вам миллионов разных других способов.
А вы полагаетесь на то, что она вам не отдастся никогда.
Если она вам не отдалась на старте процесса — то она вам не отдастся уже никогда, поверьте. С библиотеками чуть сложнее, но даже и там вероятность того, что кто-то получит эту страничку но потом-таки отдаст её вам исчезающе мала. Но если уж вам и с этой веростностью мириться не хочется — перекройте malloc и ::operator new для вашей библиотеки.

А ещё лучше использовать положительные коды ошибок, (как предложил chabapok) так как все современные OS несколько килобайт в районе NULL'а резервируют сами, без всяких ваших вмещательств.
Вообще-то, я и есть chabapok. Но вобщем, это сейчас не важно, т.к. речь о том, что в ядре под ошибки выделяют именно верхний диапазон. Я специально посмотрел код последнего стабильного ядра.

Вы оперируете таким понятием, как «исчезающе малая вероятность». Действительно, программисты иногда полагаются на вероятности. Например, uuid считается, что практически никогда не повторится, и им реально пользуются, и работает он хорошо.
Но по моему мнению, нету никаких оснований считать вероятность пренебрежимо маленькой в нашем конкретном примере.

Сами посудите:
— архитектур бывает много. Ядро же собирают не только под х86 и х86_64, но и под еще около 10архитектур.
— memory manager-ов бывает тоже много. Некоторые из них работают по приницпу чанков, которые циклически выделяются. Если будет такой и страница в него попадет — она обязательно отдастся в приложение когда-нибудь в обозримом будущем. К тому же достоверно известно, что отдача ранее использованных страниц — это потенциальная возможность кражи паролей, которую разработчики не игнорируют.
— вообще необязательно, чтобы речь шла о памяти. Например, в arm верхнее адресное пространство (это инфа не 100%) выделено под периферию. Т.е., адреса валидные и считай что уже выделены и используются не для ошибок. При этом в разных железках конкретная периферия может быть вшита в разные адресные пространства. Если драйвер такого периферийного устройства будет написан с использованием этих макросов, в железке устройство нельзя располагать по высоким адресам. Это совершенно неочевидный момент.

По этим причинам, тезис «если она вам не отдалась на старте процесса — то она вам не отдастся уже никогда» считаю в общем случае невалиден. По всей видимости, на х86 он работает хорошо.
Например, в arm верхнее адресное пространство (это инфа не 100%) выделено под периферию.
Это сильно зависит от семейства.

Современным Cortex-A (из ARMv7-AR и ARMv8) старший кусок 32/36/40-битного адресного пространства занимает DRAM: infocenter.arm.com/help/topic/com.arm.doc.den0001c/DEN0001C_principles_of_arm_memory_maps.pdf

Современные Cortex-M (из ARMv6-M и ARMv7-M) старший кусок 32-битного адресного пространства занимает vendor-specific peripheral: infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0203h/BEIFDEEB.html

На более древних ARM9 и ARM7, если не ошибаюсь, там тоже периферия.
А в жизни обычно приходится ковыряться вот в этом: developer.gnome.org/glib/stable/glib-Error-Reporting.html Т.е. мы имеем: код возврата, указатель на объект, который хотим получить и указатель на объект «Ошибка», если код возврата false, после которого ещё надо память освобождать. Но зато можно передавать произвольные ошибки с произвольными сообщениями об ошибках.
Недавно как раз эту штуку обсуждали на работе в конексте «всё будет systemd». Так что да, иногда зараза распространяется лучше.
Применяли похожее решение в своем проекте — функция принимала указатель на структуру прямоугольника. Все значения меньше 64к считались просто числом, и трактовались как прямоугольник с равными сторонами.
Похожее решение использует GetProcAddress — младшее слово может содержать точный номер функции из импорта вместо строки.
Это неопределённое поведение, точка.
На каком этапе? Я честно говоря не представляю где.
Единственные всюду верные предположения о значении указателей — это NULL==0 и арифметика указателей. Все остальное работает не везде, в частности на x86-64 этот код может и не сработать. А если попробовать скомпилировать его под PDP-11… Будет весело.
linux не делает всюду верных предположений. Он заточен под небольшой набор компиляторов и архитектур и завязан на их реализации. До недавнего времени собрать его чем-то отличным от gcc была проблема.
О том и речь, что применять это в прикладной программе не стоит.
Это определяется ABI. Вообще, судя по комментариям и статьям, люди спорят, забыв напрочь, что над стандартом стоит ABI.
Но зачем привязывать программу к ABI? Нафига? Стандарт для того и писали, чтобы подобной фигней не занимались.
Программа работает не в виртуальном пространстве, а на реальном железе. Всё определяется железом, если мы не рассматриваем виртуальные машины. Так что как раз стандарт ЯП побоку, если он противоречит архитектуре.
А если вашу программу потом придется портировать на какую-нибудь новую архитектуру? Вы вообще не допускаете такой возможности? А если делать это будет склонный к насилию психопат, который знает, где вы живете? ©
Ну, как видите конца света не наступило, так что и новые архитектуры, будучи изобретёнными, будут учитывать особенности и стандартов, и де факто существующих на рынке ОС.
А зачем тащить за собой мусор? Зачем, когда можно изначально не извращаться? Вот из за таких вот «хакеров» мы и имеем IA-32, ACPI, Microsoft Office Document Format и прочие ужасы. Расстреливать таких вредителей надо.
Стандарт писали для написания переносимых программ. Но переносимость — она штука относительная. Если вас всякие HP-UXы не интересуют и вполне хватает поддержки Linux/MacOS/Windows, то описанный в статье приём годится вполне. Особенно если использовать не отрицательные коды ошибок, как в ядре, а положительные.
Может быть вы перепутали неопределённое поведение с поведением, зависящим от реализации, вопросительный знак.
А что это за диапазон такой странный — который третий по счету? это же пределами 32 бит.
Получается, такое может работать только на 64битном ядре, или как?

И еще такой вопрос: что значит ваш пассаж «люди начали ими пользоваться!»? Неужели Торвальдс принял от вас коммит с такими изменениями?
Так это у вас в посте не тире, а знак минус? Тогда понятно.
В сорцах "-" (минус), парсер формы поставил "—" тире автоматически. Исправил на raw code. Спасибо!
А что насчет второго вопроса? Ну пускай не Торвальдс, но неужели кто-то это принял?

Лучше тогда уж так:
0 — это null
1...MAX_ERRNO — это ошибки. может даже они поместятся в диапазон традиционного null.
все остальное — это валидные адреса.

потому, что ваш метод с 64 битной архитектурой в лучшем случае некрасив, а в худшем нерабочий.
Ваше предложение — годится для userspace. Там оно, пожалуй, даже предпочтительнее. А в ядре просто везде ошибки отрицательные (и возвращаются из syscall'а) и ошибки при создании объектов — тоже.
Диапазон читается как
(0xffffffff-MAX_ERRNO+1; 0xffffffff)
Левое число меньше 2^32.
Есть ещё старый трюк с tagged pointers, но он работает только для структур с подходящим выравниванием. Т.е. указатель на char не пометишь. Зато при должной осторожности вполне подходит и для юзер-спейса (и довольно часто используется в рантаймах языков высокого уровня).
На x86_64 есть (пока что) лишние шестнадцать битов в указателях, можно их пустить на код ошибки.
Ну это вообще неприлично. Эти шестнадцать битов — это особенность даже не платформы, а микроархитектуры, их в любой момент могут убрать.
Как прострелить себе ногу бесплатно без СМС.
За такие статьи для широкой аудитории я бы давал ридонли на год.
Ну, значит, Торвальдс сидел бы в ro. :)

заглянте в linux-3.19.1/tools/virtio/linux/err.h
Я наверное погорячился. Пусть каждый стрельнёт себе в ногу. Пурка па?
Сишников никогда не останавливала опасность выстрелить себе в ногу! И если ты это сделал — ты ссзб. Если хочется уберечься от самострела, используйте java :)

А вообще конечно странно. Мне тоже несовсем ясно, как подобное оказалось в ядре. Допустим даже на х86 оно рабочее, но ведь ядро собирают под множество архитектур. И в какой-нибудь из них верхние адреса памяти вполне могут быть задействованы под валидные данные.

И в какой-нибудь из них верхние адреса памяти вполне могут быть задействованы под валидные данные.
С какого перепугу? Ядро само решает — что и куда оно будет класть. Вариантов ровно два: либа эта страничка занята обычной памятью — и тогда просто ядро потеряет 4K этой памяти, чтобы валидные объекты там не образовывались, либо там какая-нибудь хитрая хрень — и тогда обычные объекты там точно быть размещены не могут. В обоих случаях всё гарантированно работает.

Я думаю это всё же контроль ошибок для объектов ядра. А объекты берутся явно не с потолка, для них сначала выделяют память через kmalloc, который не должен выделять адреса из пространства, зарезервированного под коды ошибок, так как сам возвращает коды ошибок из этого диапазона — круг замкнулся. По сути нужно лишь гарантировать, что malloc не вернёт адрес из области 0-MAX_ERRNO, а это достаточно просто.
Ну если так, то да.

Но стоит ли сэкономленая память этого куска ~4кб вверху — это тоже вопрос, кстати. Тем более, что err выделяется обычно или в стеке, где занимает (даже с учетом теоретически возможной «нехватки» регистров процессора) никак не 4кб, или это вообще 1 ячейка на поток.

Вот кстати тоже интересный вопрос. Утверждается, что трюк позволяет убыстрить и(или) уменьшить размер в памяти. Но это лишь абстрактные слова, а где численные оценки достигнутого эффекта для разных архитектур?
Тем более, что err выделяется обычно или в стеке, где занимает (даже с учетом теоретически возможной «нехватки» регистров процессора) никак не 4кб, или это вообще 1 ячейка на поток.
Тут вопрос не в экономии на ячейках памяти, а в количестве использованных регистров.

Утверждается, что трюк позволяет убыстрить и(или) уменьшить размер в памяти. Но это лишь абстрактные слова, а где численные оценки достигнутого эффекта для разных архитектур?
А с этим у подобных трюков всегда беда. Понятно, что когда их придумывали (20 лет назад!), то скорость таки меряли. Но с тех пор и процессоры изменились и компиляторы стали умнее — а померить заново ничего нельзя: уж слишком много на этот подход теперь завязано!

С другой стороны… В ядре так просто ничего не поменять, но сделать какие-нибудь просты измерения было бы интересно… уж по крайней мере полезно это сделать перед тем как начинать этот трюк у себя в программе применять.
отведение 1 регистра под errno исключает его из некоторых мест. В результате добавляются push/pop — растет память и падает быстродействие. Но это происходит только в тех случаях, когда регистров перестает хватать. Если регистров хватает, то все остается как есть (при условии, что компилятор нормальный). А как часто случается, что регистров не хватает? Неизвестно. Это предмет для исследовательской работы.

Насчет 20лет назад. Действительно, это вполне может быть исторически сложившийся стиль.
А как часто случается, что регистров не хватает? Неизвестно. Это предмет для исследовательской работы.
Угу. Проблема в том, что эта самая «исследовательская работа» случилась через много лет после появления Linux'а. Когда Linux появился GCC более-менее сносно себя вёл на m68k (у которого 8 обычных регистров и ещё 7 адресных, которые тоже могут участвовать во многих арифметических операциях), а на x86 он очень любил всё складывать в стек.

Отсюда, кстати, и использование последней странички и многое другое. Не забудьте слова Линуса про то, что «It is NOT portable (uses 386 task switching etc), and it probably never will support anything other than AT-harddisks, as that's all I have :-(»

Никаких планов сделать что-то супер-пупер-кроссплатформенное у Линуса не было :-) Это вам не ультрасуперпереносимое ядро NT с поддержкой ажно трёх процессоров (IA32, x86-64 и ARMа), где тысячи разработчиков трахаются из-за решений сделанных для поддержки процессоров на которых операционка уже давно не работает.
для них сначала выделяют память через kmalloc, который не должен выделять адреса из пространства, зарезервированного под коды ошибок, так как сам возвращает коды ошибок из этого диапазона — круг замкнулся

Нет, kmalloc не возвращает коды ошибок в указателях. Он возвращает либо валидный указатель, либо NULL.
Не совсем в тему и больше к плюсам, для компактного и удобного выставления кодов ошибки в bool-методах классов использовал
пару процедур
bool error(int code); //всегда возвращает false
bool success(); // всегда возвращает true

и что-то типа
int lastError();


Тогда код получается довольно элегантным:
bool SomeClass::doSomething()
{
  if(something_wrong)
    return error(something_wrong_code);
  if(something_happened)
    return error(something_happened_code)
  do_something();
  return success();
}


Аналогичный код можно использовать и для методов типа void. Тогда соответственно и error() и success() возвращают void.

Кстати вот еще вопрос.

Наглядность оно улучшает. Это да. Но в статье утверждается, что этот трюк позволяет повысить производительность и(или) экономить память. Пока нету тестов, очевидно, что это утверждение является оценочным суждением.

Хочется увидеть численные результаты эффекта, достигнутого за счет использования подобного стиля. Для различных платформ/компиляторов.

Зачем два раза один и тот же комментарий?
Численные измерения можете провести сами, так как ABI утверждает только передачу первых параметров функции через регистры, остальное — через стек. Обращение к памяти гораздо дороже регистров, особенно, если при этом инвалидируется линия в кэше.
Тот камент был адресный. Не думал, что вы его прочтете.

ABI какого компилятора так делает?

Вообще-то, компилятор должен начать использовать стек только если не хватает регистров, или если в функции берется адрес переменной. И то, если такая переменная не volatile, то компилятор может заоптимайзить. Я не скажу как поступает gcc для x86 и не скажу про clang, но когда-то я смотрел какой-то компилятор для arm от texas instrumnts — и там все было максимально регистровым.

Насчет замечаний про кэш. Вызов функции (хотя это может зависеть от архитектуры), это засовывание в стек адреса возврата. А возврат из функции — это соответственно, чтение адреса возврата из стека. По этой причине, размещение дополнительной переменной в стеке в большинстве случаев не должно создавать нагрузок, связаных с кэшем, т.к. стек и так должен быть в кэше.

Конечно, если сильно постараться, то можно сделать прогу, которая только и будет делать, что подтягивать в кэш данные ради errno. Но стараться придется очень сильно :)

Мое мнение — трюк не сэкономит быстродействия (тем более в большинстве случаев там однократный вызов) и не даст сколь-нибудь стоящего выигрыша в памяти. Он использован в большей степени исключительно ради наглядности.
У меня такое впечатление, что вы не понимаете, что такое ABI.
В остальном, если сделаете такие измерения, то я обязательно вставлю ссылку в статью.
Понимаю так: abi — это интерфейс, а не реализация. Т.е., нет никакого единого стандарта для всех компиляторов и всех платформ. Есть некоторое кол-во подходов, которые зарекомендовали себя, как хорошие, но не более. В каждом компиляторе своя реализация. плюс еще есть оптимизация и прагмы, которыми можно настроить компиляцию. Необязательно, что после глубокой оптимизации аби сохранится. Хотя в нормальном компиляторе будет документирована что он сделает в оптимизациях и что нет.

Насчет измерений. Это ж не я утверждал, что подход экономит быстродействие и память. Я утверждал совсем другое — что экономии нет. Если я и сделаю какие-то измерения — они покажут именно это :)

Корректно измерить быстродействие на сегодняшних cpu не представляется возможным, т.к. время выполнения команд недокументировано. Даже на архитектурах, где заявлено что одна команда выполняется за 1 такт, на практике это бывает не всегда так.
Измерять экономию памяти — скучно. Там и так ясно, что экономии или нет, или мало. Выше высказали предположение, что реальная экономия могла быть на старых cpu до 90ых годов. Этот тезис весьма убедителен.
Понимаю так: abi — это интерфейс, а не реализация
Это, я извиняюсь, как? ABI — это ABI. Читаете документацию, там всё написано — на каких регистрах передаются аргументы, что, куда, и когда сохраняется. Это только в мире DOS разработчики могли сами себе ABI придумывать.

В каждом компиляторе своя реализация.
Не в компиляторе, а в операционной системе.

Плюс еще есть оптимизация и прагмы, которыми можно настроить компиляцию.
Прагмы могут влиять на ABI, оптимизации — разумеется, нет. Как вы результат компиляции собиратесь из нескольких модулей собирать, если вы в результате оптимизации испортили конвенцию о вызовах?

Измерять экономию памяти — скучно. Там и так ясно, что экономии или нет, или мало.
Ну уж нет. Мало того, что стек ядра за 20 лет не изменился (как был 4KiB-8KiB, так и остался), так ещё и L1 кеш процессора вырос всего-то в четыре раза за эти 20 лет (16K в PowerPC 603 в 1994м, 64K в Inte Broadwell в 2014м). Тут, извините, закон Мура не действует. От слова «совсем».
Именно то что вы дали — это же оно и есть. Вы дали ссылку на 19 разных реализаций abi.

«Как вы результат компиляции собиратесь из нескольких модулей собирать, если вы в результате оптимизации испортили конвенцию о вызовах?»

Во-первых, конвенцию о вызовах надо соблюдать только когда зовешь внешние либы. Ну и когда их компилишь. А внутри программы допустима полная анархия. Тот же clang умеет делать link time optimization, например. А в старом паскале, насколько помню, был обратный по сравнению с С порядок параметров, изза чего были проблемы с передачей переменного числа аргументов. При этом вызовы в библиотеки он дергал по тому abi, который нужен этим библиотекам, разумеется. Во-вторых компилятор может добавлять адаптеры в нужное abi. Т.е., у функции получается несколько точек входа. gcc, вероятно так не делает, но я когда-то эксперементировал с каким-то компилятором под arm, и он такое вытворял.

А стек больше не делают не потому что нет ресурсов. Просто больше ненужно. Где-то на хабре писали. что тот же apache преспокойно работает со стеком 32кб. И все равно кэшируется он кусками по сколько-то (64) байт. И все равно при вызове функции адрес возврата пишется в стек — значит он все равно будет закэширован. Но это конечно рассуждение на уровне «мне так кажется». Я не знаю как это корректно померять.
Вообще-то, компилятор должен начать использовать стек только если не хватает регистров, или если в функции берется адрес переменной.
Вы вообще не с той стороны смотрите. Проблема не в вызываемой функции, а в вызывающей. При описанном подходе все переменные чинно приходят через регистры (если использовать regparm… и да, ядро его использует) и значение возвращается тоже через регистр. А вот если пытаться вернуть два значения отдельно, то в вызывающей функции вам так или иначе нужно будет положить одну из переменных на стек — со всеми вытекающими. Не забудьте про то, что у ядра стек — всего-то 4K на всех. Дополнительная память, подо всё это задействованная, тоже ляжет на кеш. Причём тут есть ещё вот какая особенность: ядро ведь использует тот же самый кеш, что и программы (у процессора он всего один), но работает-то оно куда меньше (если у вас вдруг ядро грузит систему на 99%, то у вас случилась паталогия и вам уже ничего не поможет), так что каждый байт, который оно оттуда вытесняет куда весомее, чем в случае обычных программ (концепция микроядра на этом погорела).

Можно использовать errno в TLS, конечно, но эта концепция появилась лет через десять после описываемого трюка. И она всё равно решает только часть проблем: обращение к памяти пусть даже в L1 — это 4 такта, к регистру — 1 такт.
Ясно. Все понятно кроме одного — почему в вызывающую функцию нельзя возвратить два значения в двух регистрах, без использования стека? зачем одну из переменных понадобится класть в стек?
Потому что возврата двух переменных ABI не предусматривает, а свой компилятор разработчики Linux'а почему-то писать не захотели. На двух регистрах long longи возвращаются, в принципе можно было указатель и код ошибки запаковать в long long, но с этим проблем почти столько же, сколько с описанным в статье трюком, а эффективность и переносимость ничуть не выше.
Потому что возврата двух переменных ABI не предусматривает

Можно возвращать структуры, структура из двух полей трактуется почти как две переменные.
И тут мы потеряем в памяти и производительности (см. инвалидация кэш линий).
И тут мы потеряем в памяти и производительности

Я не агитирую за использование структур, по причинам описанным ниже, но вернуть такую структуру в регистрах можно почти во всех ABI с которыми я знаком (да, x86 тут тоже отличился).
То, что во большинстве архитектур всё сделано «хорошо», но в одной-единственной, но зато той, которая тебе нужна — по другому обозначает, что это решение Линус использовать не мог.
Есть, просто его назвали «Передача параметра по ссылке».

При достаточном уровне оптимизации происходит так:

«The compiler ignores any register definitions and allocates registers to variables and temporary values by using an algorithm that makes the most efficient use of registers.»

это из ARM Optimizing C/C++ Compiler v5.0 от ti

Эксперимент повторять лень, но суть в том, что ABI должно выполняться только при внешних вызовах. Вызовы внутри можно оптимизировать, и компилятор это делает, если его попросить.
Прочитал камент про то, что в С нет ссылок.
Всегда компилил g++ и не задумывался про это.

Получается, что да.
Есть у меня еще 1 идея — надо будет ее попробовать. Оптимизирует ли компилятор обращение через указатель на локальную переменную. Я как попробую — отпишу, если будет положительный результат.

Пока тест не провел — соглашусь с вами. Считаем, что так сделать не получится.
почему в вызывающую функцию нельзя возвратить два значения в двух регистрах

Можно, но в С это сопряжено со следующими проблемами:
— возвращаемое значение должно быть либо структурой, либо типом данных длиннее указателя (а такого типа может не быть => структура — единственный вариант).
— структура должна либо содержать void * как поле, соответствующее указателю, либо нам нужно по одной структуре на каждый тип, указатель на который мы хотим возвращать. void * чреват ошибками, когда ожидали один тип, а вернули другой, или поменяли функцию, но не всех кто её вызывает. Второй вариант в принципе можно реализовать на макросах, но он будет многословным.
Вы вопрос вы рассмотрели вырвано из контекста.

Когда-то ковырял какой-то компилятор от техас инструментс, который вполне нормально справлялся с такой задачей — передаешь по ссылке две переменные, они ложатся в разные регистры, внутри функции эти регистры меняются определенным образом, а после возврата из функции используются. То есть в полной мере — возврат из функции двух значений через регистры. Так делал он при ненулевом уровне оптимизации. При нулевой он все данные гонял через стек. Правда ковырял его в режиме EABI, а не ABI.
передаешь по ссылке две переменные

В С нет ссылок.
Sign up to leave a comment.

Articles