[Перевод] Заметки о Unix: надёжная работа с API C-библиотеки Unix возможна только из программ, написанных на C

Для того чтобы полностью реализовать требования системы верификации источника системных вызовов, разработчики OpenBSD хотят, чтобы Go выполнял бы системные вызовы через C-библиотеку, а не напрямую, из собственной среды выполнения (а у Go есть некоторые причины поступать именно так). На первый взгляд это кажется не особенно серьёзной проблемой. Это, конечно, немного неудобно, но у языка вроде Go должна быть возможность просто выполнять вызовы обычных функций из C-библиотеки, вроде open() (и использовать ABI вызова C-функций). К сожалению, не так всё просто, так как очень часто фрагменты обычного API C-библиотеки, на самом деле, реализованы в препроцессоре C. Из-за этого API C-библиотеки нельзя надёжно использовать для решения обычных задач без написания собственного связующего кода на C.

c5swh_4h3wcrh7p9obvat0xkpdw.jpeg
Звучит это, пожалуй, дико, поэтому позвольте мне проиллюстрировать это на примере всеми любимого значения errno, к которому обращаются для получения кода ошибки в том случае, если системный вызов даёт сбой (им же пользуются и для получения кодов ошибок от некоторых библиотечных вызовов). В этом материале я рассказывал о том, что в современных условиях механизм errno должен быть реализован так, чтобы у разных потоков были бы разные значения errno, так как они могут в одно и то же время выполнять различные системные вызовы. Это требует наличия у потоков собственных локальных хранилищ, а к такому хранилищу нельзя обратиться так же, как к простой переменной. Доступ к нему должен быть организован через специальный механизм, поддерживаемый средой выполнения C. Вот объявления errno из OpenBSD 6.6 и из текущей версии Fedora Linux с glibc:

/* OpenBSD */
int *__errno(void);
#define errno (*__errno())

/* Fedora glibc */
extern int *__errno_location (void) __THROW __attribute_const__;
# define errno (*__errno_location ())

В обоих этих случаях переменная errno, на самом деле, представлена определением препроцессора. Это определение ссылается на внутренние недокументированные функции C-библиотеки (на что указывают два символа подчёркивания в их именах), которые не входят в состав общедоступного API. Если скомпилировать код, написанный на C, рассчитанный на работу с этим API errno (включив в код errno.h), то он будет работать, но это — единственный официальный способ работы с errno. Нет некоей обычной переменной errno, которую можно загрузить в среде выполнения своего языка, например, после вызова функции open(). А если вызвать __errno или ____errno_location в своей среде выполнения, то это будет означать использование внутреннего API, который в будущем вполне может измениться (хотя он, вероятно, не изменится). Для того чтобы создавать надёжные среды выполнения языков программирования, которые ориентированы на общедоступный API С-библиотеки, недостаточно просто вызвать экспортированную функцию вроде open(); нужно ещё написать и скомпилировать собственную маленькую C-функцию, которая просто возвращает среде выполнения errno.

(Тут, помимо errno, могут быть и другие важные моменты; я предлагаю самостоятельно поискать их тем, кому интересна эта тема.)

Это, конечно, не какая-то новая проблема Unix. С первых дней stdio в V7 некоторые из «функций» stdio были реализованы в stdio.h в виде макросов препроцессора. Но в течение долгого времени никто не настаивал на том, чтобы единственным официально поддерживаемым способом выполнения системных вызовов было бы их выполнение из C-библиотеки, что позволяло обойти нечто вроде того, что представляет собой современный механизм errno, в тех случаях, когда не нужна совместимость с C-кодом.

(До того, как в Unix появилась многопоточность, сущность errno была представлена простой переменной и, в целом, выглядела как хороший, хотя и не идеальный интерфейс.)

Пользовались ли вы когда-нибудь недокументированными API?

tyb7fdkkt98xbfhc-jlqbuybdda.jpeg
oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru