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

Я всегда думал что функции как printf() на последнем шаге, определенном встроенном ассемблерном коде использования. Тот глубоко в кишечнике stdio.h прокладывается под землей некоторый код asm, который на самом деле говорит ЦП, что сделать. Например, в DOS, я помню, что она была реализована сначала movлуг начало строки к некоторой ячейке памяти или регистру и, чем вызов intterupt.

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

70
задан edtheprogrammerguy 11 December 2013 в 20:46
поделиться

6 ответов

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

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

1
ответ дан 24 November 2019 в 13:22
поделиться

Как библиотечная функция, такая как printf (), может быть реализована на C / C ++ без использования кода ассемблера? Что на самом деле выполняет правильное программное прерывание?

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

Поскольку вы конкретно спрашиваете о функциях C, таких как printf () , я приведу здесь небольшую трассировку, которую я сделал, чтобы выяснить , «где резина встречается с дорогой» для GNU libc. Спойлер: заканчивается на syscall () .

Системные вызовы - это не BIOS, а просто таблица пронумерованных функций с ожидаемыми параметрами, которые ОС имеет для выполнения основных услуг. В некотором смысле это похоже на «вызов чего-то по номеру, которое является согласованным соглашением с некоторыми параметрами».Хотя это отчасти и есть все программное обеспечение, поэтому мы, вероятно, должны подчеркнуть разницу: вы говорите с ОС, а не с оборудованием в реальном режиме.

Итак, вот подробное описание printf GCC ... для тех, кому нелегко скучать:

Первые шаги

Мы, конечно, начнем с прототипа 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!

Deeper Down the Rabbit Hole…

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

Таблицы в 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-параметр» __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 мы вызываем метод записи для указателя файла. Мы знаем из нашей таблицы переходов, приведенной выше, что сопоставлено с _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;
}

Теперь он просто вызывает write! Ну где для этого реализация? Вы найдете запись в 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 . C означает «отменяемый». Двоеточие отделяет тип возвращаемого значения от типов аргументов, и если вы хотите более подробное объяснение того, что они означают, вы можете увидеть комментарий в сценарии оболочки, который генерирует код, 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 :

#define __NR_write 4

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

131
ответ дан 24 November 2019 в 13:22
поделиться

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

0
ответ дан 24 November 2019 в 13:22
поделиться

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

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

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

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

16
ответ дан 24 November 2019 в 13:22
поделиться

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

-7
ответ дан 24 November 2019 в 13:22
поделиться

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

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

4
ответ дан 24 November 2019 в 13:22
поделиться
Другие вопросы по тегам:

Похожие вопросы: