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
Звучит как небольшой перебор для настолько простой операции. Хорошо хоть, что это увидят только те, кто этого действительно захочет.
