Код, создающий потоки для переменных stdin, stdout, stderr
находится в файле glibc/libio/stdio.c:
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
Переменные _IO_2_1_...
определены в файле glibc/libio/stdfiles.c как обёртки типа FILE
вокруг файловых дескрипторов 0,1,2.
Судя по вашему вопросу, вас интересует, откуда берутся файловые дескрипторы с номерами 0, 1 и 2. Ответ - из процесса предка. Процессы в Linux в большинстве своём порождаются парой системных вызовов fork/clone + execve
. Процессы, порождаемые fork
, наследуют таблицу открытых файлов предка. Системный вызов execve
так же не меняет таблицу открытых файлов (за исключением файлов, открытых с флагом O_CLOEXEC
).
Где код который создаёт стандартные потоки ввода, вывода, ошибки для нового процесса?
На ваш вопрос нет одного ответа. Всё зависит от того, как именно был порождён процесс. Например, если он был запущен из bash
, то этот код находится в файле execute_cmd.c
Если же вы сами порождаете процесс, то этот код находится в вашей программе. Вот маленький пример. Он запускает процесс с именем child.exe
и перенаправленными потоками: вход child.exe
читает из файла с именем input
, стандартный вывод пишет в файл out
, а ошибки в файл err
. Файловые дескрипторы в этом примере открываются в порождённом процессе между fork
и execv
.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t cpid, w;
int wstatus;
cpid = fork();
if (cpid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0)
{ /* Code executed by child */
int fd0=-1, fd1=-1, fd2=-1;
FILE *ch_log;
int ret = 0;
ch_log = fopen("./child.log", "w");
if (ch_log == NULL) {
perror("Failed to open log");
exit(-1);
}
fprintf(ch_log, "Hello\n");
close(0);
close(1);
close(2);
fd0 = open("./input", O_RDONLY);
if (fd0 < 0) {
fprintf(ch_log, "Failed to open input: %s\n", strerror(errno));
ret = -1;
goto close_log;
} else {
fprintf(ch_log, "Opened input: %d\n", fd0);
}
fd1 = open("./out", O_RDWR|O_CREAT|O_TRUNC);
if (fd1 < 0) {
fprintf(ch_log, "Failed to open out: %s\n", strerror(errno));
ret = -1;
goto close_log;
} else {
fprintf(ch_log, "Opened out: %d\n", fd1);
}
fd2 = open("./err", O_RDWR|O_CREAT|O_TRUNC);
if (fd2 < 0) {
fprintf(ch_log, "Failed to open err: %s\n", strerror(errno));
ret = -1;
goto close_log;
} else {
fprintf(ch_log, "Opened err: %d\n", fd2);
}
close_log:
fclose(ch_log);
if (ret < 0) {
if (fd0 >= 0) { close(fd0); }
if (fd1 >= 0) { close(fd1); }
if (fd2 >= 0) { close(fd2); }
exit(-1);
}
execl("./child.exe", (char*)NULL);
exit(ret);
}
else
{ /* Code executed by parent */
do
{
w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED);
if (w == -1)
{
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(wstatus))
{
printf("exited, status=%d\n", WEXITSTATUS(wstatus));
}
else if (WIFSIGNALED(wstatus))
{
printf("killed by signal %d\n", WTERMSIG(wstatus));
}
else if (WIFSTOPPED(wstatus))
{
printf("stopped by signal %d\n", WSTOPSIG(wstatus));
}
else if (WIFCONTINUED(wstatus))
{
printf("continued\n");
}
} while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus));
exit(EXIT_SUCCESS);
}
}
ВДОГОНКУ
Как убедиться, что stdout
инициализируется статически.
Для простоты не будем связываться с динамической линковкой, посмотрим на статически слинкованный файл. Испытания будем проводить на файле some.c
#include <stdio.h>
int main() {
FILE z = *stdout;
printf("stdout == &_IO_2_1_stdout_: %d\n", stdout == &_IO_2_1_stdout_);
printf("_flags: %o\n", z._flags);
printf("_IO_read_ptr: %p\n", z._IO_read_ptr);
printf("_IO_read_end: %p\n", z._IO_read_end);
printf("_IO_read_base: %p\n", z._IO_read_base);
printf("_IO_write_base: %p\n", z._IO_write_base);
printf("_IO_write_ptr: %p\n", z._IO_write_ptr);
printf("_IO_write_end: %p\n", z._IO_write_end);
printf("_IO_buf_base: %p\n", z._IO_buf_base);
printf("_IO_buf_end: %p\n", z._IO_buf_end);
printf("_IO_save_base: %p\n", z._IO_save_base);
printf("_IO_backup_base: %p\n", z._IO_backup_base);
printf("_IO_save_end: %p\n", z._IO_save_end);
printf("_markers: %p\n", z._markers);
printf("_chain: %p\n", z._chain);
printf("_fileno: %d\n", z._fileno);
printf("_flags2: %o\n", z._flags2);
}
Компилируем: gcc -static some.c
Получаем: ll a.out
=> 845152 Feb 22 22:03 a.out*
Запускаем:
$ ./a.out
stdout == &_IO_2_1_stdout_: 1
_flags: 37353220204
_IO_read_ptr: (nil)
_IO_read_end: (nil)
_IO_read_base: (nil)
_IO_write_base: (nil)
_IO_write_ptr: (nil)
_IO_write_end: (nil)
_IO_buf_base: (nil)
_IO_buf_end: (nil)
_IO_save_base: (nil)
_IO_backup_base: (nil)
_IO_save_end: (nil)
_markers: (nil)
_chain: 0x6b9580
_fileno: 1
_flags2: 0
Первая строка подтверждает, что stdout == &_IO_2_1_stdout_
Последующие строки печатают часть полей объекта типа FILE
, на который указывает значение символа stdout
Следующий шаг: смотрим таблицу символов получившегося файла:
$ objdump -x a.out | grep stdout
00000000006b97a0 g O .data 0000000000000008 stdout
00000000006b9360 g O .data 00000000000000e0 _IO_2_1_stdout_
00000000006b97a0 g O .data 0000000000000008 .hidden _IO_stdout
Символы stdout
и _IO_stdout
занимают 8 байтов по смещению 6b97a0
от начала файла, в секции .data
Символ _IO_2_1_stdout_
находится по смещению 6b9360
тоже в секции .data
.
Далее: смотрим, как эти символы используются в коде приложения.
$ objdump --disassemble a.out | grep -E '(<[^>]+>:|stdout)' | grep stdout -B1
0000000000400b6d <main>:
400b87: 48 8b 05 12 8c 2b 00 mov 0x2b8c12(%rip),%rax # 6b97a0 <_IO_stdout>
400caa: 48 8b 15 ef 8a 2b 00 mov 0x2b8aef(%rip),%rdx # 6b97a0 <_IO_stdout>
400cb1: 48 8d 05 a8 86 2b 00 lea 0x2b86a8(%rip),%rax # 6b9360 <_IO_2_1_stdout_>
--
000000000040f960 <_IO_printf>:
40f9d9: 48 8b 3d c0 9d 2a 00 mov 0x2a9dc0(%rip),%rdi # 6b97a0 <_IO_stdout>
--
000000000040ff60 <_IO_new_fclose>:
40fff4: 48 39 1d a5 97 2a 00 cmp %rbx,0x2a97a5(%rip) # 6b97a0 <_IO_stdout>
--
00000000004106d0 <_IO_wfile_underflow>:
41083c: 48 8b 2d 5d 8f 2a 00 mov 0x2a8f5d(%rip),%rbp # 6b97a0 <_IO_stdout>
41089f: 48 8b 3d fa 8e 2a 00 mov 0x2a8efa(%rip),%rdi # 6b97a0 <_IO_stdout>
410cbf: 48 8b 3d da 8a 2a 00 mov 0x2a8ada(%rip),%rdi # 6b97a0 <_IO_stdout>
--
0000000000412900 <_IO_new_file_underflow>:
41294a: 4c 8b 2d 4f 6e 2a 00 mov 0x2a6e4f(%rip),%r13 # 6b97a0 <_IO_stdout>
412ae4: 48 8b 3d b5 6c 2a 00 mov 0x2a6cb5(%rip),%rdi # 6b97a0 <_IO_stdout>
412b75: 48 8b 3d 24 6c 2a 00 mov 0x2a6c24(%rip),%rdi # 6b97a0 <_IO_stdout>
Символ stdout
используется в функциях main
, _IO_printf
, _IO_new_fclose
, _IO_wfile_underflow
, _IO_new_file_underflow
. Если поглядеть в дизассемблированный код, то можно глазками убедиться, что этот символ используется только для чтения. Так как приложение слинковано статически, то нет никакого другого кода, в котором переменные stdout
и _IO_2_1_stdout_
могли бы инициализироваться. То есть это данные, статически инциализированные компилятором.
Финальный шаг: подтверждение того, что stdout
- это статический указатель на _IO_2_1_stdout_
Извлечём объектный файл stdio.o
из библиотеки libc.a
: $ ar x /usr/lib/x86_64-linux-gnu/libc.a stdio.o
Теперь посмотрим таблицу символов и релокации этого объектного файла:
$ objdump -t -r stdio.o
stdio.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .data.rel 0000000000000000 .data.rel
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 g O .data.rel 0000000000000008 stderr
0000000000000000 *UND* 0000000000000000 _IO_2_1_stderr_
0000000000000000 g O .data.rel 0000000000000008 .hidden _IO_stderr
0000000000000008 g O .data.rel 0000000000000008 stdout
0000000000000000 *UND* 0000000000000000 _IO_2_1_stdout_
0000000000000008 g O .data.rel 0000000000000008 .hidden _IO_stdout
0000000000000010 g O .data.rel 0000000000000008 stdin
0000000000000000 *UND* 0000000000000000 _IO_2_1_stdin_
0000000000000010 g O .data.rel 0000000000000008 .hidden _IO_stdin
RELOCATION RECORDS FOR [.data.rel]:
OFFSET TYPE VALUE
0000000000000000 R_X86_64_64 _IO_2_1_stderr_
0000000000000008 R_X86_64_64 _IO_2_1_stdout_
0000000000000010 R_X86_64_64 _IO_2_1_stdin_
Символ stdout
занимает 8 байт по смещению 8 в секции .data.rel
, а таблица релокации предписывает записать в эту секцию по смещению 8 адрес символа _IO_2_1_stdout_
.
Итого. Когда линкер собирает исполнимый файл, в котором упоминается stdout
, он берёт этот символ из объектного файла libc.a/stdio.o
и в соответствии с таблицей релокации подставляет в значение этого символа смещение символа _IO_2_1_stdout_
из объектного файла libc.a/stdfiles.o
, то есть инициализирует stdout
как указатель на _IO_2_1_stdout_