Hello World е една от първите програми, които начинаещите програмисти пишат на който и да било език за програмиране.

При С програмката Hello World изглежда съвсем опростено и кратко:

#include 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.globlmain.type main, @function main:pushq %rbpmovq%rsp, %rbpmovl$.LC0, %edicallputspopq%rbpret

В този листинг се вижда, че не се извиква функцията printf, а puts. Функцията puts също е определена във файла stdio.h и лесно можем да видим, че нейната работа е да изведе на външно устройство текстовия ред и да върне каретката.

ОК, разбрахме коя точно е функцията, която се извиква от нашия код. Но къде е реализацията на самата puts?

За да определим коя софтуерна библиотека реализира puts, ще използваме ldd, която показва зависимостите от различните библиотеки, както и nm, която показва символите на обектния файл.

$ ldd hellolibc.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.soGNU 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_puts е нашето текущо звено във веригата извиквания на програмката hello world. Намираме нейното определяне и се вижда, че това е макрос, определен в libio/libioP.h, който извиква друг макрос, който отново… Дървото макроси изглежда по следния начин:

#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_, а необходимата ни функция се нарича __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 таблицата, директно свързана със 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 CallerSyscall nameArgsStrong nameWeak names ... write-writeCi: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, която се отнася за конкретния файл. В Linux това често се реализира като код в драйвера и сега трябва да си изясним какъв драйвер се извиква в нашия случай.

В своите експерименти използвам 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-x2 root root0 okt.10 10:14 . drwxr-xr-x 21 root root3580 okt.15 06:21 .. crw--w----1 kostty136, 0 okt.15 06:43 0 c---------1 root root5, 2 okt.10 10:14 ptmx

Командата tty извежда името на файла, прикачен към стандартния вход и както можем да видим от списъка с файлове в /proc, същият файл се използва и за извеждане, както и за потока за грешките. Тези файлови устройства в /dev/pts се наричат псевдо терминали и по-точно, това са подчинени (slave) псевдо терминали. Когато някакъв процес пише в slave псевдо терминал, данните попадат в основния (master) псевдо терминал. Master псевдо терминалът е устройството /dev/ptmx.

Драйверът за псевдо терминала се намира в Linux ядрото във файла 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 13177kos11uCHR5,20t01133 /dev/ptmx gdbus13177 13178kos11uCHR5,20t01133 /dev/ptmx dconf13177 13179kos11uCHR5,20t01133 /dev/ptmx gmain13177 13182kos11uCHR5,20t01133 /dev/ptmx ~$ ps 13177PID TTYSTATTIME COMMAND 13177 ?Sl0: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

Не е ли малко прекалено за една толкова елементарна операция? Добре е все пак, че всичко това ще видят само хората, на които това е необходимо и наистина искат да вникнат в нещата.