Определения функций C / C ++ без сборки
Я всегда думал, что такие функции, как printf()
, на последнем этапе определяются с помощью встроенной сборки. Глубоко в недрах stdio.h похоронен некоторый код asm, который на самом деле говорит CPU о том, что делать. Например, в dos, я помню, что он был реализован, сначала mov
начало строки в некоторое место или регистр памяти, а затем вызвав int
terupt.
Однако, поскольку версия x64 для Visual Studio не поддерживает встроенный ассемблер вообще, это заставило меня задуматься, как вообще не может быть никаких функций, определенных на ассемблере, в C / C ++. Как библиотека, например printf()
реализуется в C / C ++ без использования кода ассемблера? Что на самом деле выполняет правильное программное прерывание? Благодарю.
- В чем разница между «asm», «__asm» и «__asm__»?
- Преобразовать встроенный код сборки в C ++
- Эффективная функция сравнения целого числа
- x86 / x64 CPUID в C #
- Могу ли я использовать синтаксис Intel сборки x86 с GCC?
Как библиотека, например printf (), реализуется в C / C ++ без использования кода ассемблера? Что на самом деле выполняет правильное программное прерывание?
Для большинства практических целей вы не можете назвать BIOS из Linux или из Windows . И действительно, вам не следует вообще взаимодействовать с BIOS – если вы не пишете операционную систему или загрузчик.
Поскольку вы специально задаете вопрос о функциях C, таких как printf()
, то, что я здесь приведу, это небольшой отпечаток, который я сделал, чтобы узнать, «где резина соответствует дороге» для libc GNU. Предупреждение о спойлере : оно заканчивается на syscall () .
Системные вызовы – это не BIOS, а просто таблица нумерованных функций с ожидаемыми параметрами, которые ОС выполняет для выполнения основных сервисов. В некотором смысле это похоже, в смысле «вызывать что-то по числу, которое является соглашением по соглашению с некоторыми параметрами». Хотя это то, что все программное обеспечение, поэтому мы должны, вероятно, подчеркнуть разницу: вы разговариваете с ОС, а не с оборудованием в реальном режиме.
Итак, вот в чем дело, особенно в GCC’s printf
… для тех, кому нелегко скучно:
Первые шаги
Разумеется, мы начнем с прототипа printf, который определяется в файле libc/libio/stdio.h
extern int printf (__const char *__restrict __format, ...);
Однако вы не найдете исходный код для функции, называемой printf. Вместо этого в файле /libc/stdio-common/printf.c
вы найдете немного кода, связанного с функцией __printf
:
int __printf (const char *format, ...) { va_list arg; int done; va_start (arg, format); done = vfprintf (stdout, format, arg); va_end (arg); return done; }
Макрос в том же файле устанавливает связь, так что эта функция определяется как псевдоним для не-подчеркнутого printf:
ldbl_strong_alias (__printf, printf);
Имеет смысл, что printf будет тонким слоем, который вызывает vfprintf с помощью stdout. Действительно, мясо форматирования выполняется в vfprintf, которое вы найдете в libc/stdio-common/vfprintf.c
. Это довольно длительная функция, но вы можете видеть, что она все еще в C!
Глубоко вниз по кроличьей дыре …
vfprintf загадочно вызывает outchar и outstring, которые являются странными макросами, определенными в том же файле:
#define outchar(Ch) \ do \ { \ register const INT_T outc = (Ch); \ if (PUTC (outc, s) == EOF || done == INT_MAX) \ { \ done = -1; \ goto all_done; \ } \ ++done; \ } \ while (0)
Уклоняясь от вопроса о том, почему это так странно, мы видим, что он зависит от загадочного PUTC, также в том же файле:
#define PUTC(C, F) IO_putwc_unlocked (C, F)
Когда вы IO_putwc_unlocked
к определению IO_putwc_unlocked
в libc/libio/libio.h
, вы можете начать думать, что вам не все равно, как работает printf:
#define _IO_putwc_unlocked(_wch, _fp) \ (_IO_BE ((_fp)->_wide_data->_IO_write_ptr \ >= (_fp)->_wide_data->_IO_write_end, 0) \ ? __woverflow (_fp, _wch) \ : (_IO_wint_t) (*(_fp)->_wide_data->_IO_write_ptr++ = (_wch)))
Но, несмотря на то, что он немного читается, он просто выполняет буферизацию. Если в буфере указателя файла достаточно места, тогда он будет просто __woverflow
в него символ … но если нет, он вызывает __woverflow
. Поскольку единственный вариант, когда вы закончили буфер, – это очистить экран (или любое другое устройство, которое указатель вашего файла представляет), мы можем надеяться найти там магическое заклинание.
Vtables в C?
Если вы догадались, что мы будем прыгать через еще один разочаровывающий уровень косвенности, вы были бы правы. Посмотрите в libc / libio / wgenops.c, и вы найдете определение __woverflow
:
wint_t __woverflow (f, wch) _IO_FILE *f; wint_t wch; { if (f->_mode == 0) _IO_fwide (f, 1); return _IO_OVERFLOW (f, wch); }
В основном, указатели файлов реализованы в стандартной библиотеке GNU как объекты. У них есть члены данных, но также и функции, которые вы можете вызвать с помощью изменений макроса JUMP. В файле libc/libio/libioP.h
вы найдете небольшую документацию по этой технике:
/* THE JUMPTABLE FUNCTIONS. * The _IO_FILE type is used to implement the FILE type in GNU libc, * as well as the streambuf class in GNU iostreams for C++. * These are all the same, just used differently. * An _IO_FILE (or FILE) object is allows followed by a pointer to * a jump table (of pointers to functions). The pointer is accessed * with the _IO_JUMPS macro. The jump table has a eccentric format, * so as to be compatible with the layout of a C++ virtual function table. * (as implemented by g++). When a pointer to a streambuf object is * coerced to an (_IO_FILE*), then _IO_JUMPS on the result just * happens to point to the virtual function table of the streambuf. * Thus the _IO_JUMPS function table used for C stdio/libio does * double duty as the virtual function table for C++ streambuf. * * The entries in the _IO_JUMPS function table (and hence also the * virtual functions of a streambuf) are described below. * The first parameter of each function entry is the _IO_FILE/streambuf * object being acted on (ie the 'this' parameter). */
Поэтому, когда мы находим IO_OVERFLOW
в libc/libio/genops.c
, мы обнаруживаем, что это макрос, который вызывает указатель на “1-parameter” __overflow
в указателе файла:
#define IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
Таблицы переходов для различных типов указателей файлов находятся в libc / libio / fileops.c
const struct _IO_jump_t _IO_file_jumps = { JUMP_INIT_DUMMY, JUMP_INIT(finish, INTUSE(_IO_file_finish)), JUMP_INIT(overflow, INTUSE(_IO_file_overflow)), JUMP_INIT(underflow, INTUSE(_IO_file_underflow)), JUMP_INIT(uflow, INTUSE(_IO_default_uflow)), JUMP_INIT(pbackfail, INTUSE(_IO_default_pbackfail)), JUMP_INIT(xsputn, INTUSE(_IO_file_xsputn)), JUMP_INIT(xsgetn, INTUSE(_IO_file_xsgetn)), JUMP_INIT(seekoff, _IO_new_file_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_new_file_setbuf), JUMP_INIT(sync, _IO_new_file_sync), JUMP_INIT(doallocate, INTUSE(_IO_file_doallocate)), JUMP_INIT(read, INTUSE(_IO_file_read)), JUMP_INIT(write, _IO_new_file_write), JUMP_INIT(seek, INTUSE(_IO_file_seek)), JUMP_INIT(close, INTUSE(_IO_file_close)), JUMP_INIT(stat, INTUSE(_IO_file_stat)), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) }; libc_hidden_data_def (_IO_file_jumps)
Также существует #define, который _IO_new_file_overflow
с _IO_file_overflow
, а первый – в том же исходном файле. (Примечание. INTUSE – это просто макрос, который маркирует функции, используемые для внутреннего использования, это не означает ничего подобного «эта функция использует прерывание»)
Мы уже на месте?!
Исходный код для _IO_new_file_overflow делает кучу больше манипуляции с буфером, но он вызывает _IO_do_flush
:
#define _IO_do_flush(_f) \ INTUSE(_IO_do_write)(_f, (_f)->_IO_write_base, \ (_f)->_IO_write_ptr-(_f)->_IO_write_base)
Теперь мы находимся в точке, где _IO_do_write, вероятно, там, где резина действительно встречает дорогу: небуферизованная, фактическая, прямая запись на устройство ввода-вывода. По крайней мере, мы можем надеяться! Он отображается макросом в _IO_new_do_write, и мы имеем следующее:
static _IO_size_t new_do_write (fp, data, to_do) _IO_FILE *fp; const char *data; _IO_size_t to_do; { _IO_size_t count; if (fp->_flags & _IO_IS_APPENDING) /* On a system without a proper O_APPEND implementation, you would need to sys_seek(0, SEEK_END) here, but is is not needed nor desirable for Unix- or Posix-like systems. Instead, just indicate that offset (before and after) is unpredictable. */ fp->_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return 0; fp->_offset = new_pos; } count = _IO_SYSWRITE (fp, data, to_do); if (fp->_cur_column && count) fp->_cur_column = INTUSE(_IO_adjust_column) (fp->_cur_column - 1, data, count) + 1; _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base; fp->_IO_write_end = (fp->_mode <= 0 && (fp->_flags & (_IO_LINE_BUF+_IO_UNBUFFERED)) ? fp->_IO_buf_base : fp->_IO_buf_end); return count; }
К сожалению, мы снова застряли … _IO_SYSWRITE
делает работу:
/* The 'syswrite' hook is used to write data from an existing buffer to an external file. It generalizes the Unix write(2) function. It matches the streambuf::sys_write virtual function, which is specific to this implementation. */ typedef _IO_ssize_t (*_IO_write_t) (_IO_FILE *, const void *, _IO_ssize_t); #define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN) #define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)
Поэтому внутри do_write мы вызываем метод write в указателе файла. Мы знаем из нашей таблицы прыжка выше, которая сопоставляется с _IO_new_file_write, так что это?
_IO_ssize_t _IO_new_file_write (f, data, n) _IO_FILE *f; const void *data; _IO_ssize_t n; { _IO_ssize_t to_do = n; while (to_do > 0) { _IO_ssize_t count = (__builtin_expect (f->_flags2 & _IO_FLAGS2_NOTCANCEL, 0) ? write_not_cancel (f->_fileno, data, to_do) : write (f->_fileno, data, to_do)); if (count < 0) { f->_flags |= _IO_ERR_SEEN; break; } to_do -= count; data = (void *) ((char *) data + count); } n -= to_do; if (f->_offset >= 0) f->_offset += n; return n; }
Теперь он просто звонит! Ну где же это реализация? Вы найдете запись в libc/posix/unistd.h
:
/* Write N bytes of BUF to FD. Return the number written, or -1. This function is a cancellation point and therefore not marked with __THROW. */ extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;
(Примечание: __wur
– макрос для __attribute__ ((__warn_unused_result__)))
Функции, созданные из таблицы
Это всего лишь прототип для написания. Вы не найдете файл write.c для Linux в стандартной библиотеке GNU. Вместо этого вы найдете методы, специфичные для платформы, для подключения к функции записи ОС по-разному, все в каталоге libc / sysdeps /.
Мы будем следить за тем, как Linux это делает. Существует файл sysdeps/unix/syscalls.list
который используется для автоматического создания функции записи. Соответствующие данные из таблицы:
File name: write Caller: “-” (ie Not Applicable) Syscall name: write Args: Ci:ibn Strong name: __libc_write Weak names: __write, write
Не все таинственное, за исключением Ci:ibn
. С означает «аннулировать». Двоеточие отделяет тип возвращаемого значения от типов аргументов, и если вы хотите получить более глубокое объяснение того, что они означают, тогда вы можете увидеть комментарий в сценарии оболочки, который генерирует код libc/sysdeps/unix/make-syscalls.sh
.
Итак, теперь мы ожидаем, что будем иметь возможность связываться с функцией, называемой __libc_write, которая генерируется этим скриптом оболочки. Но что генерируется? Некоторый C-код, который реализует запись через макрос, называемый SYS_ify, который вы найдете в sysdeps / unix / sysdep.h
#define SYS_ify(syscall_name) __NR_##syscall_name
Ах, старый старый токен: P. Таким образом, реализация этого __libc_write
становится не чем иным, как прокси-вызовом функции syscall с параметром __NR_write
и другими аргументами.
Где заканчивается тротуар …
Я знаю, что это было захватывающее путешествие, но теперь мы находимся в конце GNU libc. Это число __NR_write
определяется Linux. Для 32-разрядных архитектур X86 вы получите linux/arch/x86/include/asm/unistd_32.h
к linux/arch/x86/include/asm/unistd_32.h
:
#define __NR_write 4
Единственное, на что нужно обратить внимание, это реализация syscall. Что я могу сделать в какой-то момент, но пока я просто перечислил некоторые ссылки на то, как добавить системный вызов в Linux .
Во-первых, вы должны понимать концепцию колец.
Ядро работает в кольце 0, то есть имеет полный доступ к памяти и кодам операций.
Программа работает обычно в кольце 3. Она имеет ограниченный доступ к памяти и не может использовать все коды операций.
Поэтому, когда для программного обеспечения требуются дополнительные привилегии (для открытия файла, записи в файл, выделения памяти и т. Д.), Он должен запрашивать kernel.
Это можно сделать разными способами. Программные прерывания, SYSENTER и т. Д.
Давайте рассмотрим пример программных прерываний с функцией printf ():
1 – Ваше программное обеспечение вызывает printf ().
2 – printf () обрабатывает вашу строку и args, а затем необходимо выполнить функцию ядра, поскольку запись в файл не может быть выполнена в кольце 3.
3 – printf () генерирует программное прерывание, помещая в регистр номер функции ядра (в этом случае функцию write ()).
4 – Выполнение программного обеспечения прерывается, и указатель инструкции переходит к коду ядра. Итак, теперь мы находимся в кольце 0, в функции ядра.
5 – Ядро обрабатывает запрос, записывая его в файл (stdout является файловым дескриптором).
6 – Когда все закончится, kernel вернется к коду программного обеспечения, используя инструкцию iret.
7 – Код программного обеспечения продолжается.
Таким образом, функции стандартной библиотеки C могут быть реализованы в C. Все, что нужно сделать, – это знать, как вызвать kernel, когда ему нужно больше привилегий.
Стандартные функции библиотеки реализованы в базовой библиотеке платформы (например, UNIX API) и / или прямыми системными вызовами (которые все еще являются функциями C). Системные вызовы (на платформах, о которых я знаю), внутренне реализуемые вызовом функции с встроенным asm, который помещает номер системного вызова и параметры в регистры процессора и запускает прерывание, которое затем обрабатывает kernel.
Существуют также другие способы связи с оборудованием, помимо системных вызовов, но они обычно недоступны или, скорее, ограничены при работе в современной операционной системе или, по крайней мере, позволяют им требовать некоторых системных вызовов. Устройство может отображаться в памяти, поэтому запись на определенные адреса памяти (с помощью обычных указателей) управляет устройством. Также часто используются порты ввода-вывода, и в зависимости от архитектуры, к которой они обращаются с помощью специальных кодов процессора, они также могут быть привязаны к определенным адресам.
В Linux утилита strace
позволяет вам видеть, какие системные вызовы выполняются программой. Итак, принимая такую программу
int main () { Е ( "х"); return 0; }
Скажем, вы скомпилируете его как printx
, затем strace printx
дает
execve ("./ printx", ["./printx"], [/ * 49 vars * /]) = 0 brk (0) = 0xb66000 access ("/ etc / ld.so.nohwcap", F_OK) = -1 ENOENT (Нет такого файла или каталога) mmap (NULL, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000 access ("/ etc / ld.so.preload", R_OK) = -1 ENOENT (Нет такого файла или каталога) open ("/ etc / ld.so.cache", O_RDONLY | O_CLOEXEC) = 3 fstat (3, {st_mode = S_IFREG | 0644, st_size = 119796, ...}) = 0 mmap (NULL, 119796, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa6dc0c7000 close (3) = 0 access ("/ etc / ld.so.nohwcap", F_OK) = -1 ENOENT (Нет такого файла или каталога) open ("/ lib / x86_64-linux-gnu / libc.so.6", O_RDONLY | O_CLOEXEC) = 3 (3, "\\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 2 \ 0 \ 0 \ 0 \ 0 \ 0 "..., 832) = 832 fstat (3, {st_mode = S_IFREG | 0755, st_size = 1811128, ...}) = 0 mmap (NULL, 3925208, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_DENYWRITE, 3, 0) = 0x7fa6dbb06000 mprotect (0x7fa6dbcbb000, 2093056, PROT_NONE) = 0 mmap (0x7fa6dbeba000, 24576, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_DENYWRITE, 3, 0x1b4000) = 0x7fa6dbeba000 mmap (0x7fa6dbec0000, 17624, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0) = 0x7fa6dbec0000 close (3) = 0 mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c6000 mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c5000 mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c4000 arch_prctl (ARCH_SET_FS, 0x7fa6dc0c5700) = 0 mprotect (0x7fa6dbeba000, 16384, PROT_READ) = 0 mprotect (0x600000, 4096, PROT_READ) = 0 mprotect (0x7fa6dc0e7000, 4096, PROT_READ) = 0 munmap (0x7fa6dc0c7000, 119796) = 0 fstat (1, {st_mode = S_IFCHR | 0620, st_rdev = makedev (136, 0), ...}) = 0 mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e4000 написать (1, "x", 1x) = 1 exit_group (0) =?
Каучук встречается с дорогой (сортировка, см. Ниже) в конце последнего вызова трассы: write(1,"x",1x)
. На этом этапе управление переходит от пользовательского printx
к ядру Linux, который обрабатывает остальные. write()
– это функция-shell, объявленная в unistd.h
extern ssize_t write (int __fd, __const void * __ buf, size_t __n) __wur;
Большинство системных вызовов завернуты таким образом. Функция-shell, как следует из названия, представляет собой нечто большее, чем тонкий слой кода, который помещает аргументы в правильные регистры, а затем выполняет программное прерывание 0x80. Ядро ловушки прерывания, а остальное – история. Или, по крайней мере, так оно и работало. По-видимому, накладные расходы на прерывание прерываний были довольно высокими, и, как отмечалось ранее, современные архитектуры процессоров ввели sysenter
сборке sysenter
, которая на тот же результат достигает скорости. На этой странице « Системные вызовы» есть довольно хорошее резюме того, как работают системные вызовы.
Я чувствую, что вы, вероятно, немного разочарованы этим ответом, как и я. Очевидно, что в некотором смысле это ложное дно, так как между вызовом write()
и точка, в которой буфер frameworks видеокарты фактически модифицирован, чтобы на экране появилась буква «x». Масштабирование в точке контакта (чтобы остаться с аналогами «резина против дороги»), погрузившись в kernel, обязательно будет образовательным, если потребуется много времени. Я предполагаю, что вам придется путешествовать по нескольким уровням абстракции, таким как буферизованные выходные streamи, персональные устройства и т. Д. Обязательно публикуйте результаты, если вы решите следить за этим:
Ну, все операторы C ++, за исключением точки с запятой и комментариями, становятся машинным кодом, который сообщает CPU, что делать. Вы можете написать собственную функцию printf, не прибегая к сборке. Единственными операциями, которые должны быть записаны в сборке, являются ввод и вывод из портов, а также то, что включает и отключает прерывания.
Однако assembly по-прежнему используется для программирования уровня системы по соображениям производительности. Несмотря на то, что встроенная assembly не поддерживается, нет ничего, что помешало бы вам написать отдельный модуль в сборке и связать его с вашим приложением.
В общем, библиотечная функция предварительно скомпилирована и распределяет рекламный объект. Inline-ассемблер используется только в конкретной ситуации по соображениям производительности, но это исключение, а не правило. На самом деле, printf мне не кажется хорошим кандидатом на сборку. Insetad, функционирует как memcpy или memcmp. Очень низкоуровневые функции могут быть скомпилированы родным ассемблером (masm? Gnu asm?) И распределяются как объект в библиотеке.
Компилятор генерирует сборку из исходного кода C / C ++.