Ответ 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.