Хабрахабр

[Перевод] Анализ сишного Hello World

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

Функция puts также определена в файле stdio.h и занимается тем, что печатает строку и перенос строки.
Хорошо, мы поняли, какую функцию на самом деле вызывает наш код. Из ассемблерного листинга видно, что вызывается не printf, а puts. Но где puts реализована?

Чтобы определить, какая библиотека реализует puts, используем ldd, выводящий зависимости от библиотек, и nm, выводящую символы объектного файла.

$ ldd hello libc.so.6 => /lib64/libc.so.6 (0x0000003e4da00000)
$ nm /lib64/libc.so.6 | grep " puts"
0000003e4da6dd50 W puts

6 на моей системе (Fedora 19). Функция находится в сишной библиотеке, называемой libc, и расположенной в /lib64/libc.so. 6 — симлинк на /usr/lib64/libc-2. В моём случае, /lib64 — симлинк на /usr/lib64, а /usr/lib64/libc.so. Это файл и содержит все функции.
Узнаем версию libc, запустив файл на выполнение, как будто он исполнимый:
17.so
.

$ /usr/lib64/libc-2.17.so GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al.
...

17. В итоге, наша программа вызывает функцию puts из glibc версии 2. 17. Давайте теперь посмотрим, что делает функция puts из glibc-2.

Заглянув в код, видим следующее в libio/ioputs.c:
В коде glibc достаточно сложно ориентироваться из-за повсеместного использования макросов препроцессора и скриптов.

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)

Давайте я просто объясню, что тут происходит? Глаза болеть. В нашем случае таблица лежит в структуре, называемой _IO_2_1_stdout_, a нужная нам функция называется __xsputn. Glibc использует jump-table для вызова функций.

Структура объявлена в файле 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, так что мы должны найти код. Это — вполне стандартный способ записи байтов в файл по файловому дескриптору.

Большинство системных вызовов, обёрнутых в glibc, генерируются из таких файлов. Я нашёл код write в sysdeps/unix/syscalls.list. Тело функции создаётся из общего шаблона системных вызовов.
Файл содержит имя функции и аргументы, которые она принимает.

# File name Caller Syscall name Args Strong name Weak names
...
write - write Ci:ibn __libc_write __write write
...

Код ядра гораздо читабельнее glibc. Когда glibc код вызывает write (либо __libcwrite, либо __write), происходит syscall в ядро. Точка входа в syscall write находится в fs/readwrite.c:

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
return ret;
}

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

Это, в частности, означает, что мой терминал по умолчанию — gnome-terminal. Я использую для экспериментов Fedora 19 с Gnome 3. Запустим этот терминал и сделаем следующее:

~$ 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

Звучит как небольшой перебор для настолько простой опреации. Хорошо хоть, что это увидят только те, кто этого действительно захочет.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть