Подтвердить что ты не робот

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

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

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

4b9b3361

Ответ 1

Вы, конечно, правы, что резина должна соответствовать дороге в точке some. Но там много слоев, чтобы пройти, прежде чем вы сможете найти это место! Похоже, у вас есть некоторые предубеждения, основанные на днях DOS, и это не слишком актуально.

Здесь были некоторые хорошие общие моменты, но никто не связывался с точными дьяволами в деталях источника. Поэтому, чтобы вы достаточно сожалели о том, что вы спросили:) Я сделал исчерпывающий след истории printf для GNU libc и Linux.. пытаясь не волноваться о любом из шагов. В процессе я обновил некоторые свои знания (ПРЕДУПРЕЖДЕНИЕ: это не для скуки!):

(Исходная ссылка http://blog.hostilefork.com/where-printf-rubber-meets-road/, и она будет поддерживаться там. Но для предотвращения гниения ссылок здесь находится содержимое, кэшированное.)

Первые шаги

Ну, конечно, начните с прототипа для 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 в 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. Поскольку единственный вариант, когда вы выходите из буфера, должен скрываться на экране (или на любом устройстве, которое представляет ваш указатель на файл), мы можем надеяться найти там магическое заклинание.

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 (i.e. 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: "-" (i.e. Not Applicable)
Syscall name: write
Args: Ci:ibn
Strong name: __libc_write
Weak names: __write, write

Не все таинственное, кроме Ci:ibn. С означает "аннулировать". Двоеточие отделяет тип возвращаемого значения от типов аргументов, и если вы хотите более глубокое объяснение того, что они означают, тогда вы можете увидеть комментарий в оболочке script, которая генерирует код, libc/sysdeps/unix/make-syscalls.sh.

Итак, теперь ожидали, что смогут ссылаться на функцию с именем __libc_write, которая создается этой оболочкой script. Но что генерируется? Некоторый 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:

#define __NR_write 4

Единственное, на что нужно обратить внимание, это реализация syscall. Что я могу сделать в какой-то момент, но на данный момент Ill просто указывает на некоторые ссылки на как добавить системный вызов в Linux.

Ответ 2

Во-первых, вы должны понимать концепцию колец.
Ядро работает в кольце 0, то есть имеет полный доступ к памяти и кодам операций.
Программа работает обычно в кольце 3. Она имеет ограниченный доступ к памяти и не может использовать все коды операций.

Поэтому, когда для программного обеспечения требуются дополнительные привилегии (для открытия файла, записи в файл, выделения памяти и т.д.), Ему необходимо запросить ядро.
Это можно сделать разными способами. Программные прерывания, SYSENTER и т.д.

Давайте рассмотрим пример программных прерываний с функцией printf():
1 - Ваше программное обеспечение вызывает printf().
2 - printf() обрабатывает вашу строку и args, а затем необходимо выполнить функцию ядра, поскольку запись в файл не может быть выполнена в кольце 3.
3 - printf() генерирует программное прерывание, помещая в регистр номер функции ядра (в этом случае функцию write()).
4 - Выполнение программного обеспечения прерывается, и указатель инструкции переходит к коду ядра. Итак, теперь мы находимся в кольце 0, в функции ядра.
5 - Ядро обрабатывает запрос, записывая его в файл (stdout - файловый дескриптор).
6 - Когда все закончится, ядро ​​вернется к программному коду, используя инструкцию iret.
7 - Программный код продолжается.

Таким образом, функции стандартной библиотеки C могут быть реализованы в C. Все, что нужно сделать, это знать, как вызвать ядро, когда ему нужно больше привилегий.

Ответ 3

Стандартные функции библиотеки реализуются в базовой библиотеке платформы (например, UNIX API) и/или прямыми системными вызовами (которые все еще являются функциями C). Системные вызовы (на платформах, о которых я знаю), внутренне реализуемые вызовом функции с встроенным asm, который помещает номер системного вызова и параметры в регистры процессора и запускает прерывание, которое затем обрабатывает ядро.

Существуют также другие способы общения с оборудованием, помимо системных вызовов, но они обычно недоступны или, скорее, ограничены при работе в современной операционной системе или, по крайней мере, для их использования, требуют некоторых системных вызовов. Устройство может отображаться в памяти, поэтому запись на определенные адреса памяти (с помощью обычных указателей) управляет устройством. Также часто используются порты ввода-вывода и в зависимости от архитектуры, к которой они обращаются с помощью специальных кодов процессора, или они также могут быть привязаны к определенным адресам.

Ответ 4

Ну, все операторы С++, за исключением точки с запятой и комментариями, становятся машинным кодом, который сообщает CPU, что делать. Вы можете написать собственную функцию printf, не прибегая к сборке. Единственными операциями, которые должны быть записаны в сборке, являются ввод и вывод из портов, а также то, что включает и отключает прерывания.

Тем не менее, сборка по-прежнему используется в системном программировании по соображениям производительности. Несмотря на то, что встроенная сборка не поддерживается, нет ничего, что помешало бы вам написать отдельный модуль в сборке и связать его с вашим приложением.

Ответ 5

В Linux утилита strace позволяет вам видеть, какие системные вызовы выполняются программой. Итак, принимая такую ​​программу

    int main(){
    printf("x");
    return 0;
    }

Скажем, вы скомпилируете его как printx, затем strace printx дает

    execve("./printx", ["./printx"], [/* 49 vars */]) = 0
    brk(0)                                  = 0xb66000
    access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
    mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000
    access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
    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 (No such file or directory)
    open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
    read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\30\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
    write(1, "x", 1x)                        = 1
    exit_group(0)                           = ?

Каучук встречается с дорогой (сортировка, см. ниже) в конце последнего вызова трассы: write(1,"x",1x). На этом этапе управление переходит от user-land printx к ядру Linux, который обрабатывает остальные. write() - это функция-оболочка, объявленная в unistd.h

    extern ssize_t write (int __fd, __const void *__buf, size_t __n) __wur;

Большинство системных вызовов завернуты таким образом. Функция-оболочка, как следует из названия, представляет собой нечто большее, чем тонкий слой кода, который помещает аргументы в правильные регистры, а затем выполняет программное прерывание 0x80. Ядро ловушки прерывания, а остальное - история. Или, по крайней мере, так, как он работал. По-видимому, накладные расходы на прерывание прерываний были довольно высокими, и, как отмечалось ранее, современные архитектуры процессоров представили инструкцию по сборке sysenter, которая выполняет тот же результат на скорости. Эта страница Системные вызовы содержит довольно хорошее резюме того, как работают системные вызовы.

Я чувствую, что вы, вероятно, немного разочаруетесь в этом ответе, как и я. Очевидно, что в некотором смысле это ложное дно, так как между вызовом и точка, в которой фактически настроен буфер кадра видеокарты, чтобы на экране появилась буква "x". Масштабирование в точке контакта (чтобы остаться с аналогами "резина против дороги" ), погрузившись в ядро, обязательно будет образовательным, если потребуется много времени. Я предполагаю, что вам придется путешествовать по нескольким уровням абстракции, таким как буферизованные выходные потоки, символьные устройства и т.д. Обязательно опубликуйте результаты, если вы решите следить за этим:)

Ответ 6

В общем, библиотечная функция предварительно скомпилирована и распределяет рекламный объект. Inline-ассемблер используется только в конкретной ситуации по соображениям производительности, но это исключение, а не правило. На самом деле, printf мне не кажется хорошим кандидатом на сборку. Insetad, функционирует как memcpy или memcmp. Очень низкоуровневые функции могут быть скомпилированы родным ассемблером (masm? Gnu asm?) И распределены как объект в библиотеке.

Ответ 7

Компилятор создает сборку из исходного кода C/С++.