O Hello World é um dos primeiros programas que escrevemos em qualquer linguagem de programação.
Para C, o hello world parece simples e curto:
#include <stdio.h> void main() { printf("Hello World!\n"); }
Como o programa é tão curto, deve ser fundamental explicar o que está acontecendo "sob o capô".
Primeiro, vamos ver o que acontece ao compilar e vincular:
gcc --save-temps hello.c -o hello
--save-temps
adicionado para que o gcc deixe o
hello.s
, um arquivo de código de montagem.
Aqui está o código do assembler de exemplo que recebi:
.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
Como você pode ver na lista do assembler, não é
printf
que é chamado, mas
puts
. A função
puts
também é definida no arquivo
stdio.h
e está comprometida em imprimir uma linha e quebra de linha.
Bem, entendemos que função nosso código realmente chama. Mas onde é implementado o
puts
?
Para determinar qual biblioteca implementa as
puts
, usamos
ldd
, que exibe dependências da biblioteca, e
nm
, que exibe os caracteres do arquivo de objeto.
$ ldd hello libc.so.6 => /lib64/libc.so.6 (0x0000003e4da00000) $ nm /lib64/libc.so.6 | grep " puts" 0000003e4da6dd50 W puts
A função está localizada em uma biblioteca chamada
libc
e localizada em
/lib64/libc.so.6
no meu sistema (Fedora 19). No meu caso,
/lib64
é um link simbólico para
/usr/lib64
e
/usr/lib64/libc.so.6
é um link simbólico para
/usr/lib64/libc-2.17.so
. Este arquivo contém todas as funções.
Descobrimos a versão do
libc
executando o arquivo como se fosse executável:
$ /usr/lib64/libc-2.17.so GNU C Library (GNU libc) stable release version 2.17, by Roland McGrath et al. ...
Como resultado, nosso programa chama a função de
glibc
da
glibc
versão 2.17. Vamos agora ver o que a função
glibc-2.17
faz na
glibc-2.17
.
O código glibc é difícil de navegar devido ao uso generalizado de macros e scripts de pré-processador. Observando o código, vemos o seguinte em
libio/ioputs.c
:
weak_alias (_IO_puts, puts)
Na glibc, isso significa que, ao chamar
_IO_puts
,
_IO_puts
é realmente chamado. Esta função é descrita no mesmo arquivo e a parte principal da função é semelhante a esta:
int _IO_puts (str) const char *str; {
Joguei todo o lixo em torno do importante desafio para nós. Agora
_IO_sputn
é o nosso elo atual na cadeia de chamadas hello world. Encontramos uma definição, esse nome é uma macro definida em
libio/libioP.h
, que chama outra macro, que novamente ... A árvore de macro contém o seguinte:
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
Que diabos está acontecendo aqui? Vamos expandir todas as macros para ver o código final:
((*(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)
Olhos doem. Deixe-me explicar o que está acontecendo aqui. Glibc usa jump-table para chamar funções. No nosso caso, a tabela está em uma estrutura chamada
_IO_2_1_stdout_
, e a função que precisamos é chamada
__xsputn
.
A estrutura é declarada no arquivo
libio/libio.h
:
extern struct _IO_FILE_plus _IO_2_1_stdout_;
E no arquivo
libio/libioP.h
definições da estrutura, tabela e seu campo:
struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };
Se aprofundarmos ainda mais, veremos que a tabela
_IO_2_1_stdout_
inicializada no arquivo
libio/stdfiles.c
, e as implementações
libio/stdfiles.c
das funções da tabela são definidas em
libio/fileops.c
:
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS); # define _IO_new_file_xsputn _IO_file_xsputn
Tudo isso significa que, se usarmos a tabela de salto associada ao
stdout
, chamaremos a função
_IO_new_file_xsputn
. Já está mais perto, certo? Essa função lança dados em buffers e chama
new_do_write
quando o conteúdo do buffer pode ser gerado. É assim que
new_do_write
parece:
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; }
Obviamente, a macro é chamada. Através do mesmo mecanismo de tabela de salto que vimos para
__xsputn
,
__write
é
__write
. Para arquivos
__write
,
__write
mapeado para
_IO_new_file_write
. Essa função é chamada em última análise. Vamos olhar para ela?
_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) {
Finalmente, uma função que chama algo que não começa com um sublinhado! A função de
write
é conhecida e definida em
unistd.h
. Essa é uma maneira bastante padrão de gravar bytes em um arquivo usando um descritor de arquivo. A função de
write
é definida na própria glibc, portanto, precisamos encontrar o código.
Encontrei o código de
write
em
sysdeps/unix/syscalls.list
. A maioria das chamadas de sistema envolvidas no glibc são geradas a partir desses arquivos. O arquivo contém o nome da função e os argumentos necessários. O corpo da função é criado a partir de um padrão de chamada do sistema comum.
# File name Caller Syscall name Args Strong name Weak names ... write - write Ci:ibn __libc_write __write write ...
Quando o código glibc chama
write
(
__libcwrite
ou
__write
), syscall ocorre no kernel. O código do kernel é muito mais legível que o glibc. O ponto de entrada para a
write
syscall está em
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; }
Primeiro, a estrutura correspondente ao descritor de arquivo é encontrada e, em seguida, a função
vfs_write
é
vfs_write
no subsistema do sistema de arquivos virtual (vfs). A estrutura no nosso caso corresponderá ao arquivo
stdout
. Dê uma olhada em
vfs_write
:
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { ssize_t ret;
A função delega a execução da função de
write
pertencente a um arquivo específico. No Linux, isso geralmente é implementado no código do driver, portanto, você precisa descobrir qual driver é chamado no nosso caso.
Eu uso o Fedora 19 com o Gnome 3. Para experimentos, isso significa que meu terminal é o
gnome-terminal
por padrão. Execute este terminal e faça o seguinte:
~$ 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
O comando
tty
imprime o nome de um arquivo vinculado à entrada padrão e, como você pode ver na lista de arquivos em
/proc
, o mesmo arquivo está associado à saída e ao fluxo de erros. Esses arquivos de dispositivo em
/dev/pts
são chamados pseudo-terminais, mais precisamente, são pseudo-terminais escravos. Quando um processo grava um pseudo-terminal no escravo, os dados vão para o pseudo-terminal mestre. O pseudo-terminal mestre é um dispositivo
/dev/ptmx
.
O driver do pseudo-terminal está localizado no kernel do Linux no
drivers/tty/pty.c
:
static void __init unix98_pty_init(void) {
Ao escrever em
pts
,
pty_write
é
pty_write
, que se parece com isso:
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) { c = tty_insert_flip_string(to->port, buf, c); if (c) { tty_flip_buffer_push(to->port); tty_wakeup(tty); } } return c; }
Os comentários ajudam a entender que os dados estão na fila de entrada do pseudo-terminal principal. Mas quem está lendo essa linha?
~$ 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
O processo
gnome-terminal-server
gera todos
gnome-terminal
e cria novos pseudo-terminais. É ele quem ouve o pseudo-terminal principal e, no final, recebe nossos dados, que é
"Hello World"
. O servidor do
gnome-terminal
recebe a string e a exibe na tela. Em geral, não havia tempo suficiente para uma análise detalhada do
gnome-terminal
:)
Conclusão
O caminho geral da nossa linha "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
Parece um
pequeno fracasso para uma operação tão simples. É bom que apenas quem realmente queira o veja.