Hello World е една от първите програми, които начинаещите програмисти

...
Hello World е една от първите програми, които начинаещите програмисти
Коментари Харесай

Доколко елементарна е програмката Hello World, написана на С

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

При С програмката 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_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 , само че тук носи името __writ e. Виждаме че за файловете __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, която се отнася за съответния файл. В 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-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.

Драйверът за псевдо терминала се намира в 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 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
Източник: kaldata.com

СПОДЕЛИ СТАТИЯТА


Промоции

КОМЕНТАРИ
НАПИШИ КОМЕНТАР