Определения функций C / C ++ без сборки

Я всегда думал, что такие функции, как printf() , на последнем этапе определяются с помощью встроенной сборки. Глубоко в недрах stdio.h похоронен некоторый код asm, который на самом деле говорит CPU о том, что делать. Например, в dos, я помню, что он был реализован, сначала mov начало строки в некоторое место или регистр памяти, а затем вызвав int terupt.

Однако, поскольку версия x64 для Visual Studio не поддерживает встроенный ассемблер вообще, это заставило меня задуматься, как вообще не может быть никаких функций, определенных на ассемблере, в C / C ++. Как библиотека, например printf() реализуется в C / C ++ без использования кода ассемблера? Что на самом деле выполняет правильное программное прерывание? Благодарю.

    Как библиотека, например 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 ++.

    Давайте будем гением компьютера.