Hello World — одна из первых программ, которые мы пишем на любом языке программирования.
Для C hello world выглядит просто и коротко:
Поскольку программа такая короткая, должно быть элементарно объяснить, что происходит «под капотом».
Во-первых, посмотрим, что происходит при компиляции и линковке:
Вот примерный ассемблерный код, который я получил:
Из ассемблерного листинга видно, что вызывается не
Хорошо, мы поняли, какую функцию на самом деле вызывает наш код. Но где
Чтобы определить, какая библиотека реализует
Функция находится в сишной библиотеке, называемой
Узнаем версию
В итоге, наша программа вызывает функцию
В коде glibc достаточно сложно ориентироваться из-за повсеместного использования макросов препроцессора и скриптов. Заглянув в код, видим следующее в
На языке glibc это означает, что при вызове
Я выкинул весь мусор вокруг важного нам вызова. Теперь
Что за хрень тут происходит? Давайте развернём все макросы, чтобы посмотреть на финальный код:
Глаза болеть. Давайте я просто объясню, что тут происходит? Glibc использует jump-table для вызова функций. В нашем случае таблица лежит в структуре, называемой
Структура объявлена в файле
А в файле
Если копнуть ещё глубже, увидим, что таблица
Всё это означает, что если мы используем jump-table, связанную с
Разумеется, вызывается макрос. Через тот же jump-table механизм, что мы видели для
Наконец-то функция, которая вызывает что-то, не начинающееся с подчёркивания! Функция
Я нашёл код
Когда glibc код вызывает
Сначала находится структура, соответствующая файловому дескриптору, затем вызывается функция
Функция делегирует выполнение функции
Я использую для экспериментов Fedora 19 с Gnome 3. Это, в частности, означает, что мой терминал по умолчанию —
Команда
Драйвер для псевдотерминала находится в ядре линукса в файле
При записи в
Комментарии помогают понять, что данные попадают во входную очередь master псевдотерминала. Но кто читает из этой очереди?
Процесс
Общий путь нашей строки «Hello World»:
Звучит как небольшой перебор для настолько простой операции. Хорошо хоть, что это увидят только те, кто этого действительно захочет.
Для C hello world выглядит просто и коротко:
#include <stdio.h>
void main() {
printf("Hello World!\n");
}Поскольку программа такая короткая, должно быть элементарно объяснить, что происходит «под капотом».
Во-первых, посмотрим, что происходит при компиляции и линковке:
gcc --save-temps hello.c -o hello--save-temps добавлено, чтобы gcc оставил hello.s, файл с ассемблерным кодом.Вот примерный ассемблерный код, который я получил:
.file "hello.c"
.section .rodata
.LC0:
.string "Hello World!"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
movl $.LC0, %edi
call puts
popq %rbp
retИз ассемблерного листинга видно, что вызывается не
printf, а puts. Функция puts также определена в файле stdio.h и занимается тем, что печатает строку и перенос строки. Хорошо, мы поняли, какую функцию на самом деле вызывает наш код. Но где
puts реализована?Чтобы определить, какая библиотека реализует
puts, используем ldd, выводящий зависимости от библиотек, и nm, выводящую символы объектного файла.$ ldd hello
libc.so.6 => /lib64/libc.so.6 (0x0000003e4da00000)
$ nm /lib64/libc.so.6 | grep " puts"
0000003e4da6dd50 W putsФункция находится в сишной библиотеке, называемой
libc, и расположенной в /lib64/libc.so.6 на моей системе (Fedora 19). В моём случае, /lib64 — симлинк на /usr/lib64, а /usr/lib64/libc.so.6 — симлинк на /usr/lib64/libc-2.17.so. Этот файл и содержит все функции. Узнаем версию
libc, запустив файл на выполнение, как будто он исполнимый:$ /usr/lib64/libc-2.17.so
GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al.
...В итоге, наша программа вызывает функцию
puts из glibc версии 2.17. Давайте теперь посмотрим, что делает функция puts из glibc-2.17.В коде glibc достаточно сложно ориентироваться из-за повсеместного использования макросов препроцессора и скриптов. Заглянув в код, видим следующее в
libio/ioputs.c:weak_alias (_IO_puts, puts)На языке glibc это означает, что при вызове
puts на самом деле вызывается _IO_puts. Эта функция описана в том же файле, и основная часть функции выглядит так:int _IO_puts (str)
const char *str;
{
//...
_IO_sputn (_IO_stdout, str, len)
//...
}Я выкинул весь мусор вокруг важного нам вызова. Теперь
_IO_sputn — наше текущее звено в цепочке вызовов hello world. Находим определение, это имя — макрос, определённый в libio/libioP.h, который вызывает другой макрос, который снова… Дерево макросов содержит следующee: #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
//...
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
//...
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
//...
# define _IO_JUMPS_FUNC(THIS) \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS ((struct _IO_FILE_plus *) (THIS)) + (THIS)->_vtable_offset))
//...
#define _IO_JUMPS(THIS) (THIS)->vtableЧто за хрень тут происходит? Давайте развернём все макросы, чтобы посмотреть на финальный код:
((*(struct _IO_jump_t **) ((void *) &((struct _IO_FILE_plus *) (((_IO_FILE*)(&_IO_2_1_stdout_)) ) )->vtable+(((_IO_FILE*)(&_IO_2_1_stdout_)) )->_vtable_offset))->__xsputn ) (((_IO_FILE*)(&_IO_2_1_stdout_)), str, len)
Глаза болеть. Давайте я просто объясню, что тут происходит? Glibc использует jump-table для вызова функций. В нашем случае таблица лежит в структуре, называемой
_IO_2_1_stdout_, a нужная нам функция называется __xsputn. Структура объявлена в файле
libio/libio.h:extern struct _IO_FILE_plus _IO_2_1_stdout_;А в файле
libio/libioP.h лежат определения структуры, таблицы, и её поля:struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
//...
struct _IO_jump_t
{
//...
JUMP_FIELD(_IO_xsputn_t, __xsputn);
//...
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
//...
};Если копнуть ещё глубже, увидим, что таблица
_IO_2_1_stdout_ инициализируется в файле libio/stdfiles.c, а сами реализации функций таблицы определяются в libio/fileops.c:/* from libio/stdfiles.c */
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
/* from libio/fileops.c */
# define _IO_new_file_xsputn _IO_file_xsputn
//...
const struct _IO_jump_t _IO_file_jumps =
{
//...
JUMP_INIT(xsputn, _IO_file_xsputn),
//...
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
//...
};Всё это означает, что если мы используем jump-table, связанную с
stdout, мы в итоге вызовем функцию _IO_new_file_xsputn. Уже ближе, не так ли? Эта функция перекидывает данные в буфера и вызывает new_do_write, когда можно выводить содержимое буфера. Так выглядит 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;
..
count = _IO_SYSWRITE (fp, data, to_do);
..
return count;
}Разумеется, вызывается макрос. Через тот же jump-table механизм, что мы видели для
__xsputn, вызывается __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;
_IO_ssize_t count = 0;
while (to_do > 0)
{
// ..
write (f->_fileno, data, to_do));
// ..
}Наконец-то функция, которая вызывает что-то, не начинающееся с подчёркивания! Функция
write известная и определена в unistd.h. Это — вполне стандартный способ записи байтов в файл по файловому дескриптору. Функция write определена в самом glibc, так что мы должны найти код.Я нашёл код
write в sysdeps/unix/syscalls.list. Большинство системных вызовов, обёрнутых в glibc, генерируются из таких файлов. Файл содержит имя функции и аргументы, которые она принимает. Тело функции создаётся из общего шаблона системных вызовов.# File name Caller Syscall name Args Strong name Weak names
...
write - write Ci:ibn __libc_write __write write
...Когда glibc код вызывает
write (либо __libcwrite, либо __write), происходит syscall в ядро. Код ядра гораздо читабельнее glibc. Точка входа в syscall write находится в fs/readwrite.c:SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
if (ret >= 0)
file_pos_write(f.file, pos);
fdput(f);
}
return ret;
}Сначала находится структура, соответствующая файловому дескриптору, затем вызывается функция
vfs_write из подсистемы виртуальной файловой системы (vfs). Структура в нашем случае будет соответствовать файлу stdout. Посмотрим на vfs_write:ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
//...
ret = file->f_op->write(file, buf, count, pos);
//...
return ret;
}Функция делегирует выполнение функции
write, принадлежащей конкретному файлу. В линуксе это часто реализовано в коде драйвере, так что надо бы выяснить, какой драйвер вызовется в нашем случае.Я использую для экспериментов Fedora 19 с Gnome 3. Это, в частности, означает, что мой терминал по умолчанию —
gnome-terminal. Запустим этот терминал и сделаем следующее:~$ tty
/dev/pts/0
~$ ls -l /proc/self/fd
total 0
lrwx------ 1 kos kos 64 okt. 15 06:37 0 -> /dev/pts/0
lrwx------ 1 kos kos 64 okt. 15 06:37 1 -> /dev/pts/0
lrwx------ 1 kos kos 64 okt. 15 06:37 2 -> /dev/pts/0
~$ ls -la /dev/pts
total 0
drwxr-xr-x 2 root root 0 okt. 10 10:14 .
drwxr-xr-x 21 root root 3580 okt. 15 06:21 ..
crw--w---- 1 kos tty 136, 0 okt. 15 06:43 0
c--------- 1 root root 5, 2 okt. 10 10:14 ptmxКоманда
tty выводит имя файла, привязанного к стандартному вводу, и, как видно из списка файлов в /proc, тот же файл связан с выводом и потоком ошибок. Эти файлы устройств в /dev/pts называются псевдотерминалами, точнее говоря, это slave псевдотерминалы. Когда процесс пишет в slave псевдотерминал, данные попадают в master псевдотерминал. Master псевдотерминал — это девайс /dev/ptmx.Драйвер для псевдотерминала находится в ядре линукса в файле
drivers/tty/pty.c:static void __init unix98_pty_init(void)
{
//...
pts_driver->driver_name = "pty_slave";
pts_driver->name = "pts";
pts_driver->major = UNIX98_PTY_SLAVE_MAJOR;
pts_driver->minor_start = 0;
pts_driver->type = TTY_DRIVER_TYPE_PTY;
pts_driver->subtype = PTY_TYPE_SLAVE;
//...
tty_set_operations(pts_driver, &pty_unix98_ops);
//...
/* Now create the /dev/ptmx special device */
tty_default_fops(&ptmx_fops);
ptmx_fops.open = ptmx_open;
cdev_init(&ptmx_cdev, &ptmx_fops);
//...
}
static const struct tty_operations pty_unix98_ops = {
//...
.open = pty_open,
.close = pty_close,
.write = pty_write,
//...
};При записи в
pts вызывается pty_write, которая выглядит так:static int pty_write(struct tty_struct *tty, const unsigned char *buf, int c)
{
struct tty_struct *to = tty->link;
if (tty->stopped)
return 0;
if (c > 0) {
/* Stuff the data into the input queue of the other end */
c = tty_insert_flip_string(to->port, buf, c);
/* And shovel */
if (c) {
tty_flip_buffer_push(to->port);
tty_wakeup(tty);
}
}
return c;
}Комментарии помогают понять, что данные попадают во входную очередь master псевдотерминала. Но кто читает из этой очереди?
~$ lsof | grep ptmx
gnome-ter 13177 kos 11u CHR 5,2 0t0 1133 /dev/ptmx
gdbus 13177 13178 kos 11u CHR 5,2 0t0 1133 /dev/ptmx
dconf 13177 13179 kos 11u CHR 5,2 0t0 1133 /dev/ptmx
gmain 13177 13182 kos 11u CHR 5,2 0t0 1133 /dev/ptmx
~$ ps 13177
PID TTY STAT TIME COMMAND
13177 ? Sl 0:04 /usr/libexec/gnome-terminal-serverПроцесс
gnome-terminal-server порождает все gnome-terminal'ы и создаёт новые псевдотерминалы. Именно он слушает master псевдотерминал и, в итоге, получит наши данные, которые "Hello World". Сервер gnome-terminal получает строку и отображает её на экране. Вообще, на подробный анализ gnome-terminal времени не хватило :)Заключение
Общий путь нашей строки «Hello World»:
0. hello: printf("Hello World")
1. glibc: puts()
2. glibc: _IO_puts()
3. glibc: _IO_new_file_xsputn()
4. glibc: new_do_write()
5. glibc: _IO_new_file_write()
6. glibc: syscall write
7. kernel: vfs_write()
8. kernel: pty_write()
9. gnome_terminal: read()
10. gnome_terminal: show to userЗвучит как небольшой перебор для настолько простой операции. Хорошо хоть, что это увидят только те, кто этого действительно захочет.
