Анализ сишного Hello World

Original author: Kjetil Østerås
  • Translation
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

Из ассемблерного листинга видно, что вызывается не 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_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)

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

Я нашёл код 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, принадлежащей конкретному файлу. В линуксе это часто реализовано в коде драйвере, так что надо бы выяснить, какой драйвер вызовется в нашем случае.

Я использую для экспериментов 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.

Драйвер для псевдотерминала находится в ядре линукса в файле 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

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

Comments 140

    +22
    И эти люди ругают нас за наворачивание абстракции над абстракцией.
      +3
      Так тут всё вполне просто, никаких наворотов.
        +8
        В том и дело, что в системе итак достаточно абстракций, хватит плодить ещё больше.
          +9
          Проблема не в том, что мы порождаем абстракции, а в том, что мы порождаем абстракции для решения проблем, уже решённых существующими абстракциями! Вот это — настоящий перпетум мобиль, с которым нужно-таки бороться.
          +3
          Я всегда подозревал, что-то не то с этими сями…

          Поэтому:
          image
            +13
            Собственно, «C» закончился вызовом библиотечной функции.
            Бейсик, думаете сможет сделать что-то лучше?
              +4
              Это был сарказм
                +3
                Вообще, если говорить о урощении, то можно же напрямую дернуть системный вызов write()
                void main() {
                    write(1, "Hello World!\n", 13);
                }
                


                В этом случае мы сразу сразу пропускаем пункты с 0 по 6 из заключения статьи.

                P.S.
                Правда в этом случае линтер ругаться будет если мы не заинклудим unistd.h
                Но в этом случае достаточно просто определить этот write.

                В итоге это:
                int write(int fildes, const void *buf, int nbytes);
                
                void main() {
                    write(1, "Hello World!\n", 13);
                }
                

                И компилится и работает без каких либо проблем.
                  +4
                  Нужны бенчи и тесты :) по самому быстрому выполнению хелловорлда :)
                    0
                    Компилируются и сегфолтился )
                    int main;


                    Просто забавный оффтоп
                      +1
                      (gdb) disassemble
                      Dump of assembler code for function main:
                      => 0x0000555555755014 <+0>: add %al,(%rax)
                      0x0000555555755016 <+2>: add %al,(%rax)
                      End of assembler dump.
                      (gdb) i r rax
                      rax 0x555555755014 93824994332692

                      double main;
                      тоже компилится.

                      А на
                      long long long main;

                      ругается
                      aaa.c:1:11: error: ‘long long long’ is too long for GCC
                        0
                        add %al,(%rax)

                        Т.е. оно компилится в нули.
                          0
                          Да. А значение rax говорит, что на нули поток выполнения прыгнул с помощью подобной команды перехода jmp (%rax)\call(%rax)\j...(%rax).
                      0
                      напрямую дернуть системный вызов write()

                      на самом деле вы вызвали glibc-обёртку для системного вызова.

                    –1

                    Зачем же номера строк до сих пор писать?

                      +19
                      Чтобы можно было написать GOTO 10
                        +2
                        Или GOSUB 10 :)
                          0
                          resume тогда будет нужен еще
                          0

                          Для GOSUB интерпретатор QB64 позволяет использовать текстовые метки.

                            0
                            Ну, если текстовые метки есть, то и для GOTO они тоже подойдут и номера строк не нужны.
                          0
                          А зачем писать GOTO 10, если можно написать

                          DO
                          ..
                          LOOP WHILE TRUE


                          или
                          DO WHILE TRUE
                          ..
                          LOOP


                          Dmitri-D
                          Ну, если так же развернуть для какого-нибудь Quick Basic, или старого MSCC под DOS, то там будет простое ah=9h, int21h. В рамках «посмотреть, как под капотом работает HW для новичка» — да, это нагляднее, чем десять оберток над обертками. Не лучше или хуже; нагляднее. Но это так, отвлеченный коммент в сторону — ведь эта статья как раз имеет задачу показать эту самую цепочку, а не просто внутреннее устройство.
                              0
                              В таком случае — да, но просто метка 10, как традиционно метка первой строки, не кажется подходящей для вашей интерпретации. Скорее, изначально имелся в виду бесконечный цикл в классическом 10 print… goto 10.
                              А для выхода из цикла можно так же текстовую метку использовать.
                                0
                                :) Ну тогда можно просто — 10 GOTO 10
                                И только исключение поможет.
                            0

                            Господи, какая ностальгия напала...

                          0
                          Зачем подчекивание в строке 20?
                          Зачем табуляция перед 30?
                          Зачем 50 END?

                          Зачем заставлять писать на бэйские человека для которого это не первый язык программирования?
                          +7
                          Хорошо, это был файловый дескриптор, ассоциированный с терминалом. А мог быть stdin другого приложения или вообще символьное устройство /dev/null. Т.е. другими словами printf, как и puts — это разновидность межпроцессного взаимодействия. Что если не ядро должно заниматься межпроцессным взаимодействием?
                          Так что не вижу никаких излишеств.
                            +10
                            Спасибо, я теперь могу совершенно чётко сформулировать, за что я люблю embedded…
                              +16

                              Ну, конкретно для отладочного printf в embedded путь может быть ещё длиннее)

                                +3
                                Если он отладочный, то пофиг. А другого printf в embedded просто нет.Хотя нет, отладочного тоже нет. :)
                                Недавно на эту тему на linkedin:
                                — Чем отличается C от Embedded C?
                                — Нет printf?
                                — Нет malloc!
                                  +2
                                  А вот я бы не стал так утверждать, ибо регулярно использую sprintf :-)
                                  Но да я знаю что он за собой тянет, тем не менее писать свой вариант sprintf еще более накладно. А нужно для того же вебсервера регулярно (и не только его, запись в файлы, общение с gsm-модулями и прочим, прочим). Да, можно и иначе сделать, но тогда придется что-то заметно урезать. Пока хватает ресурсов — применение sprintf весьма оправданно. ИМХО конечно. Например в высоконагруженном месте печать float сделал по собственной схеме, что позволило существенно ускорить процесс.
                                    0

                                    Я sprintf побаиваюсь, предпочитаю snprintf.

                                      0
                                      Вы абсолютно правы. Но практически у меня бывали и исключения, хотя вот вспоминаешь и думаешь — а не вернуться ли и не переделать ли :-)
                                    0
                                    Хм, надвно наблюдал терминал оплаты сбера с ошибкой malloc, хотя это тоже должен быть embedded…
                                      +1
                                      В первую очередь, это Сбер :D
                                      Вообще, это не совсем эмбеддед или совсем не ембеддед.
                                      Ещё ~100 лет назад в банкоматах стояла OS/2
                                        +2
                                        Не совсем верно написал.
                                        Я про такую железку говорил
                                        image
                                          +1
                                          Так то не Сбер, а Верифон, с его прошивками. Да и там такой себе эмбеддед вроде, это довольно мощная железка уже. Вот например чип банковской карты тоже эмбеддед, в котором вполне себе Java приложение, да ещё и не одно.
                                            +1
                                            Вот например чип банковской карты тоже эмбеддед, в котором вполне себе Java приложение, да ещё и не одно.
                                            Там такая себе Java, спцфцкая. Без GC и free — отличный подход к выделению памяти! Malloc там, фактически, аналог Sbrk…
                                  +2
                                  За то, что весь этот путь информации хоть куда нибудь наружу надо проделывать самостоятельно? Даже если устройством показа пользователю является осциллограф на ножке GPIO.
                                    0
                                    Это было увлекательное занятие программировать 24-процессоную DSP систему реального времени, в которой к каждому процессору подключено по одному светодиоду. Процессоров было больше, 24 было только у меня. А наши мужики ещё умудрились найти багу в проприетарной ОС!

                                    Не стОит недооценивать осциллограф! Вот например, вывод логотипа альмаматери по следующему методу:
                                    Растровый цифровой логотип загружатеся в память микроконтроллера.
                                    С помощью ЦАП формируется аналоговый сигнал строчной развертки.
                                    Цифровой осциллограф оцифровывает аналоговый сигнал.
                                    Сигнал развёртки преобразуется в растровое изображение на экране.


                                    А вообще, мне этого GPIO, бывает, очень не хватает на ПК.
                                      0
                                      Не стОит недооценивать осциллограф!

                                      Всё ждал, пока они там тетрис запустят.
                                        +6
                                        Можно и тетрис.
                                        Вообще, есть такой жанр творчества: генерируется звуковой файл, к аудиовыходу компа подключается скоп и нажимается Play…
                                    +1
                                    Да-да, старый добрый эмбеддед. Сейчас как раз сижу с Code Composer Studio 9 и пытаюсь понять почему printf в консоль не гадит, хотя два дня назад вполне успешно это делал. Это я уже молчу, что нужно было специфически поставить опции на странице Debug, иначе отладчик вообще отваливается на попытке загрузить программу в процессор.
                                    +3
                                    Когда то очень давно, выдрал из glibc одну функцию и таскаю её уже страшно посчитать сколько лет уже. Но точно помню, что когда выдирал — такого кошмара там точно ещё не было:)
                                      +1

                                      А точно именно из glibc? На днях как раз всплывал printf, который другие (?) авторы выдернули, но не из glibc, а из ядра Linux — полагаю, glibc им тоже пришла в голову первой, но чем-то им исходник glibc не понравился (хотя, возможно, и тем, что исходник ядра у них на машине уже был).

                                      +8

                                      И мы ещё даже не дошли до процессов, которые при этом происходят в железе.

                                        +7

                                        А в конце выпишем гамильтониан всего этого дела и решим уравнение Шрёдингера, чтоб узнать результат. :)

                                          0
                                          Аналитически не решим, придётся численно. А там опять компьютеры, а внутри снова код приложения, код в ОС, процессы в транзисторах в CPU, снова гамильтонианы… :)
                                        +1
                                        Так здесь 2-10 к ОС относится, компилируйте под DOS :)

                                        … не через gcc, разумеется.
                                          +2
                                          компилируйте под DOS :)
                                          … не через gcc, разумеется.
                                          а почему бы и нет-то, DJGPP же есть…
                                            0
                                            DJGPP это про защищённый режим и DPMI, абстракций там сравнимо будет, по идее.
                                          0
                                          В виндовом gcc замена printf на puts не происходит:

                                          call _printf

                                          Однако замена в линуховом gcc просто знаковая. Выходит, программа Hello world не такая образцовая, если в единственной исполняемой строчке исполняется не то, что написано.
                                            +4
                                            Выходит, программа Hello world не такая образцовая, если в единственной исполняемой строчке исполняется не то, что написано.

                                            Исполняется именно то, что написано. Это основное требование к компиляторам. Просто в данном конкретном случае компилятор знает, что puts делает ровно то же самое, что и printf, но гораздо быстрее.
                                              +4
                                              Скажем так: в качестве примера первой «самой простой» программы Керниган взял самую сложную и неоднозначную функцию, которая к тому же не выполняется. Вот такой Hello world.
                                                +2

                                                Нет. В качестве примера первой «самой простой» программы Керниган взял программу, которая выводит одну строку и завершается. Именно это написано в тексте программы и именно это делает программа после компиляции и выполнения. И делает это вполне просто и однозначно с точки зрения программиста, пишущего Hello World.


                                                А то, каким образом она это делает под капотом — к Си как к языку никоим образом не относится. Это детали реализации компилятора, стандартной библиотеки и операционной системы.


                                                А если так сильно хочется попричитать по поводу "слишком сложного и неоднозначного" hello world, то вы идите дальше — жалуйтесь на то, как неоднозначно выполняется код на разных архитектурах процессоров. А какие сложные квантовые эффекты в чипе процессора этот якобы "простой" код вызывает! Жуть! Ужасный пример Керниган взял, слишком уж всё сложно и неоднозначно.

                                                  0
                                                  Ну так и написал бы puts(). Но нет, хотелось козырнуть printf(), которая тут вообще, оказывается, не стояла. Ричи, кстати, должен был знать — он же компилятор точил.
                                                    0
                                                    Возможно в примерах часто был нужен именно форматированный вывод. Наверное поэтому и использовал printf.
                                                      +4
                                                      Сам Керниган признаётся во вступлении, что «не копенгаген» в ассемблере, оно и видно. А Hello world зря взяли в «самые простые» программы. У Строструпа она выглядит совсем кошмарно — cout << «Hello world». Этим надо заканчивать курс СПП, а не начинать.
                                                        0
                                                        В свое время форматные строки для printf и scanf стали причиной, по которой я выбрал учить C++, а не С (для С взял книжку не K&R, что, конечно, было ошибкой).

                                                        Но вообще, в последнем издании Страуструпа, где он учит программированию на примере С++ (http://www.stroustrup.com/Programming/), он явно не ставит себе цели сразу объяснить, как оно все работает внутри — а наоборот, призывает использовать удобные абстракции, так что cout здесь вполне себе адекватно выглядит.
                                                          +1
                                                          Ну, да в С++ вообще нет форматирования. Попытки заменить printf(«Value1: 0x%08X, value2: %d, value3: 0x%08X\n», val1, val2, val3); превращаются в ужасную колбасу из << и модификаторов стримов.

                                                          И если я правильно помню, то стрим запоминает все модификаторы и потом в явном виде нужно откатить все обратно?
                                                            0

                                                            Это правда, но на тот момент (я не умел программировать вообще), возможность не разбираться (и не ошибаться) в форматной строчке для меня была намного важнее — std::cout и std::cin казались намного более понятными в использовании, чем printf/scanf.

                                                              0
                                                              Ну, да в С++ вообще нет форматирования.
                                                              Есть же какое-то .)

                                                              Можно через форматирование строки.
                                                                0
                                                                Есть же какое-то .)

                                                                Так я ж и написал про длинную колбасу из модификаторов стримов: ширину задай, заполняющий символ задай, основание системы счисления задай. А потом верни все обратно — т.е. снова задай, но уже значения по умолчанию.
                                                                  0
                                                                  Cтроки с форматированием могут быть опасными.
                                                                    0
                                                                    Я надеюсь что все более-менее опытные разработчики знают что в строке форматирования должна быть константа.
                                                                      0
                                                                      Да, все верно.
                                                                    0
                                                                    Библиотека в C++ здесь создавалась из расчёта, чтобы исключить парсинг форматной строки в рантайме — этот парсинг переложен на программиста при написании ввода-вывода, громоздко, но эффективнее.
                                                                    Конкретные решения в духе «setw сбрасывается при каждой форматной операции, а остальные настройки — нет» могут быть странными, да. Но printf никто не запрещал, если кому лениво (мне обычно да — даже в глубоко C++ коде предпочитаю C-style I/O, если нет явных причин так не делать).

                                                                    Если кому нужен эффективный printf-like, то для него есть Boost.Format — тот разбирает форматную спецификацию при компиляции. Заодно там ещё вкусностей (типа эффективное формирование строки, чтобы можно было, например, подробный exception сделать без промежуточного ostringstream).
                                                                    +4
                                                                    Ну на эту тему Хорстман обстебался в своём The March of Progress

                                                                    1980: C
                                                                    printf("%10.2f", x);

                                                                    1988: C++
                                                                    cout << setw(10) << setprecision(2) << fixed << x;

                                                                    1996: Java
                                                                    java.text.NumberFormat formatter = java.text.NumberFormat.getNumberInstance();
                                                                    formatter.setMinimumFractionDigits(2);
                                                                    formatter.setMaximumFractionDigits(2);
                                                                    String s = formatter.format(x);
                                                                    for (int i = s.length(); i < 10; i++) System.out.print(' ');
                                                                    System.out.print(s);

                                                                    2004: Java
                                                                    System.out.printf("%10.2f", x);

                                                                    2008: Scala and Groovy
                                                                    printf("%10.2f", x)

                                                                    (Thanks to Will Iverson for the update. He writes: “Note the lack of semi-colon. Improvement!”)

                                                                    2012: Scala 2.10
                                                                    println(f"$x%10.2f")

                                                                    (Thanks to Dominik Gruntz for the update, and to Paul Phillips for pointing out that this is the first version that is checked at compile time. Now that's progress.)
                                                                      0
                                                                      Развитие идет по спирали :)

                                                                      PS. Оригинальная статья(Hell World) у меня не открывается без прокси.
                                                                        0
                                                                        Десять лет назад, когда я на эту штуку впервые набрёл (или примера из 20012го года ещё не было) — это было ещё смешнее.

                                                                        Особенно «Note the lack of semi-colon. Improvement!»
                                                                          0
                                                                          Да, тогда можно было подумать, что это тупик :)
                                                                        0

                                                                        2020: C++20


                                                                        std::cout << std::format("{:10.2f}", x);
                                                              +2

                                                              Еще раз. Тот факт, что вызов printf в данном случае транслируется в вызов puts вызван не языком С, а компилятором gcc (конкретной версии, использованной автором) и glibc версии 2.17. Когда Ричи с Керниганом работали над Си и хеллоуворлдом для него — ни gcc, ни glibc еще и в помине не было.

                                                                0
                                                                И как это отменяет тот факт, что «на его месте должен был быть я» puts()?
                                                                  0

                                                                  Кому должен? Поведение puts и printf в данном случае одинаково. printf при этом более универсален в общем случае.


                                                                  Чем конкретно он хуже puts-а в контексте хеллоуворлда и си как языка?

                                                                    0
                                                                    Чем конкретно он (она — printf()) хуже puts-а в контексте хеллоуворлда и си как языка?

                                                                    1. тем, что это одна из самых сложных и неоднозначных функций Си (для первого примера — худший выбор)
                                                                    2. тем, что она здесь не нужна (для любого примера — странный выбор)
                                                                    3. тем, что она тупо зря гоняет процессор/препроцессор в поисках подстановок %i, которых там нет (для отдельного примера неважно, но для постоянного использования — плохой выбор.)
                                                                      +2
                                                                      3. тем, что она тупо зря гоняет процессор/препроцессор в поисках подстановок %i, которых там нет

                                                                      Так она и не гоняет — компилятор направляет по пути наименьшего сопротивления — подменяет на puts.
                                                                      А вот если бы в форматной строке были переменные — таки да, погнало бы по большому кругу и ассемблерный код был бы значительно сложнее.
                                                                      Просто одна из моделей оптимизации компилируемого кода.
                                                                        0
                                                                        Товарищ выше намекает, что в славные 70-е компилятор не подменял. А виндовый gcc и сейчас не подменяет. Но это не суть, а на сдачу. Суть в первых двух пунктах.
                                                                          0
                                                                          Первые 2 пункта верны на все 100.
                                                                          Помнится, обучение сям начиналось с puts/gets, и только через пару-тройку месяцев переходили к комбайнам printf/scanf.
                                                                            0

                                                                            Аминь, брат! Только из-за Hello world эту истину приходиться отстаивать, как Джордано Бруно.

                                                                        0

                                                                        В контексте хеллоуворлда printf выступает выступает в качестве стандартного способа вывода текста в стандартный поток вывода. В реальном мире она этим и является — стандартным способом вывода текста, используемым по-умолчанию, если нет каких-то особых требований.


                                                                        Какие преимущества использования узкоспециализированного puts? Процессор впустую подстановки не ищет? Так он и так не ищет, современный компилятор вон оптимизирует такое, как показано в статье. Универсальная printf "не нужна", а специализированная puts нужна? Так это не так работает. В 90% случаев используют общепринятый инструмент прежде всего, а на специализированные переходят если на то есть причины. И "вот конкретно эту вещь специализированный тоже может сделать" — это не причина.


                                                                        одна из самых сложных и неоднозначных функций Си

                                                                        При форматированном выводе — да, может быть сложно вспомнить/разобраться во всех этих спецификаторах форматов и типов. Но при выводе строки она не сложнее puts. А по однозначности даже получше будет — не дописывает "самовольно" переводы строк в поток.

                                                                          +1
                                                                          В своё время было очень много взломов из-за того, что люди привыкали использовать printf где ни попадя вместо puts. В конце концов получался printf вывода пользователя или неожиданно вылезал в строке %, и всё падало. Т.е. в данном случае новичков сразу учат плохому, которое потом надо будет выправлять.
                                                                            0
                                                                            Лучше выправлять некоторую небезопасность, заставляя уже наученного работника использовать безопасные версии printf, чем учить его всему форматированному выводу.
                                                                        0
                                                                        printf тратит время на парсинг форматной строки во время исполнения, puts — нет. Замена printf на puts, в данном случае — оптимизация компилятора. А любая оптимизация увеличивает время компиляции, т.е. здесь вы платите временем компиляции. Ну и плюс ко всему, если очень хочется печатать статик строку через printf лучше сделать это так: printf( "%s", «Hello world» ); Привычка всегда использовать printf с указанием форматной строки улучшит безопасность ваших программ.
                                                                          0
                                                                          Ну если вы запихнете при запуске приложения в LD_PRELOAD библиотеку, которая экспортирует puts, но не printf, то поведение при такой оптимизации поменяется. Понятно, что на практике это маловероятно, но все же.

                                                                          Откуда gcc может быть уверен, что это именно printf и puts из стандартной библиотеки? Или их такое переопределение считается как UB?
                                                                            +1
                                                                            Или их такое переопределение считается как UB?
                                                                            Их поведение описано в стандарте. Если вы его меняете, то получаете среду несовместимую со стандартом — и тут уже говорить о том, UB это или не UB смысла не имеет.
                                                                              +1
                                                                              > Откуда gcc может быть уверен, что это именно printf и puts из стандартной библиотеки?

                                                                              Из того, что вы при компиляции не давали флаг -ffreestanding.
                                                                              Не давали => сборка под hosted => libc со стандартными свойствами, в которых printf и puts взаимозаменяемы описанным образом (а fprintf и fputs — чуть иначе, но тоже).

                                                                              Там, где это не так, можно собирать для freestanding (так делают, например, во FreeBSD для ядра, rtld и ещё немного специфических компонентов).
                                                                          –2
                                                                          Вы, кстати, зря не считаете компилятор частью языка. Фортран — первый компилируемый язык — вообще включает компиляцию в название. То, что это часть сменная, не значит, что это не часть.
                                                                            +5

                                                                            Есть спецификация языка. В случае Си — это стандарт ISO/IEC 9899. Все, что не входит в эту спецификацию частью языка не является. Компилятор (и уж тем более его конкретная реализация) туда не входит. Стандартная библиотека входит, но её реализация — не входит.

                                                                              –3
                                                                              Поменяется компилятор — поменяются правила языка. Все эти стандарты — просто бумажки. Рулит компилятор, поэтому разделять его с языком — ошибка, точнее, идеализация. То же самое с HTML — есть поддержка в браузере (интерпретаторе) — есть в языке. Нет — гуляй с пляжа.
                                                                                +3
                                                                                Если чего-то есть в спеке, но нет в компиляторе — то это повод зафайлить бегу и компилятор (или, гораздо реже, спеку) исправят… а вот если чего-то было в компиляторе, а потом пропало… то это, в лучшем случае, вызовет реакцию типа «как-же всё-таки хорошо, что вот это вот всё, что вы написали — это не наша проблема».
                                                                                  0
                                                                                  Стандарт языка придумали не просто так.

                                                                                  Так все и было. Сначала был один компилятор. Потом их стало три. Каждый со своими правилами. И тогда вместо одного языка стало три. Умные дяди почесали затылки и выкатили стандарт на язык. Именно на язык, а не на конкретный компилятор.

                                                                                  И сказали так: О, Компиляторы! Вы цари и боги, и вольны делать все что хотите, покуда не нарушаете стандарт, иначе несоответствующими стандарту заклеймлены будете.

                                                                                  Таким образом, стандарт — это, конечно, просто бумажка. Но компилятор должен этой бумажке соответствовать. Для того она и существует.
                                                                                    –1
                                                                                    Не могу понять, к чему вы это написали. Я как раз утверждал, что язык и компилятор — единое целое. Изначальное управляемое видением автора языка, теперь — стандартом. И получил три минуса два раза.

                                                                                    Вы мне пишите, что стандарт нужен, чтобы «компилятор соответствовал языку», то есть сохранялась целостность языка и компилятора, то есть чтобы они были «единым целым». Зачем? Это ведь моё изначальное утверждение.
                                                                                      0
                                                                                      Язык си может быть обработан интерпретатором.
                                                                                      И это не будет нарушением стандарта.

                                                                                      Собственно, я думаю, причина всех этих минусов в том, что вы в своём посте включили компилятор в язык.

                                                                                      Язык он сферический в вакууме. Стандарт создан, конечно, с расчетом на то, как именно язык будут использовать, но записан он очень оторвано от реализации.

                                                                                      Завтра у нас появится транслятор, скармливающий программы на языке в надмашинный мирровой квазигипермозг, который посмотрев на код будет без всякой компиляции сразу выдавать результат выполнения…

                                                                                      И это никак не повлияет на сам язык.
                                                                                        –2
                                                                                        Причина минусов в том, что можно не думать, а действовать.

                                                                                        Язык без компилятора (интерпретатора, транслятора) недееспособен. Поэтому они — целое, что и вы вроде подтвердили, а вроде и нет.

                                                                                        Компиляцию в язык включил не я, а авторы первого высокоуровневого языка Фортран — фор[мула +] тран[сляция]. Но зачем держать в голове глупые факты.
                                                                                        +1
                                                                                        Я как раз утверждал, что язык и компилятор — единое целое.
                                                                                        Что, собственно, и является грубой ошибкой. У каждого компилятора есть куча особенностей, которые использовать нельзя.

                                                                                        Например первые компиляторы располагали переменные в стеке подряд и если вы писали
                                                                                        int a[4], b;
                                                                                        то вы могли обращаться к b, как к a[4]. Но частью это языка не было и поломались, в общем, довольно скоро. А ещё там можно было к intу прибавлять единичку пока не получится -1 — но, опять-таки, в стандарте это запрещено и в современных компиляторах не работает.

                                                                                        Да, компилятор, несомненно, описывает некоторый язык — но это не C! У него есть много свойств, которые в спеку не входят и полагаться на которые опасно. Потому что завтра, даже просто на другой машине, компилятор может повести себя по-другому — и вы получите кучу проблем.

                                                                                        А вот спека — она неизменна. Не зависит ни от машины, ни от желания левой пятки разработчика компилятора.

                                                                                        Изначальное управляемое видением автора языка, теперь — стандартом.
                                                                                        В любом компиляторе, всегда, есть вещи, которые туда автор не планировал закладывать — но они там есть. Просто «потому что так получилось». Опираться на них нельзя. Вот поэтому язык — это, в первую очередь, спека, а во-вторую компилятор.

                                                                                        В других языках (скажем Java) прилагаются очень серьёзные усилия к тому, чтобы программист не смог «усмотреть» в компиляторе чего-то, чего нет в спеке, в C/C++ — такие усилия не прилагаются. Правильная программа, соответствующая спеке — обязана работать, неправильная — тоже может работать, но что она, при этом будет делать — разработчиков не волнует от слова совсем. И это принципиальная позиция разработчиков. Потому в Java можно сказать, почти не покривив душой, что компилятор и язык — это одно и то же, но в C/C++ — нельзя.
                                                                    0
                                                                    А если добавить ещё вывод времени в лог…
                                                                      0
                                                                      Это прекрасно. Мне бы эту статью лет десять надад.
                                                                        0
                                                                        10 не выйдет, только 5 с лишним
                                                                        Hello World Analysis
                                                                        11 Oct 2013
                                                                        0

                                                                        Почему бы не реализовать функцию puts(const char *) посредством write(...), зачем идти таким сложным путём?

                                                                          +2
                                                                          Тут есть два момента. Во первых puts работает с буфферизацией, то есть прежде чем выдать что-то во внешний мир, он сбрасывает данные в буффер, копит их там, а потом уже выдаёт все разом. Это делается для минимизации количества системных вызовов в частности и операций в пространстве ядра — драйверах вообще.

                                                                          Но это пол беды. Логика работы с буфером находится в динамической библиотеке, доступ к которой пользовательская программа имеет через таблицу указателей на функции.

                                                                          Вот и получается, что мы сначала делаем jump в динамическую либу, потом кидаем данные в буффер… И, только если левая пятка сподобится, вызываем write…

                                                                          Насколько такая цепочка оправдана… Вопрос открытый.
                                                                            0
                                                                            На это наверняка были веские основания, к сожалению, те люди, которые их знали, на смогут поделится с нами по веским причинам — они все эти основания забыли.
                                                                            Так что единственное доставшееся нам объяснение: «У нас так принято».
                                                                              +3
                                                                              Это решение родилось в процессе эволюции системы. Тут как по Энштейну. Настолько просто, насколько возможно, но ничуть не проще.
                                                                                –2
                                                                                Поправьте: «Эйнштейн».
                                                                                +1

                                                                                Эволюция — это отнюдь не гарантия простоты и эффективности.

                                                                                  0
                                                                                  Я имею сказать по этому поводу. Эволюции надо рассматривать в долгосрочной перспективе. Неэфективные решения эволюционно нестабильны. Через миллион лет все может поменяться.
                                                                                    0
                                                                                    Неэфективные решения эволюционно нестабильны

                                                                                    Вы меня огорчаете. У Homo sapiens (как и к всех позвоночных после рыб) та же "проблема", разве что не столь ярко выраженная. Вы правда считаете, что из-за этого чертова нерва эволюция откатится назад к рыбам и начнет по новой?

                                                                                0
                                                                                На самом деле, никто не мешает написать myprintf напрямую через write. Это будет прекрасно работать. Можно даже обойтись без glibc совсем. Но glibc предлагает решение среднее по больнице, которое подходит в 98% случаев. И да, оно зачастую избыточно, но работает как часы…
                                                                            0
                                                                            а как оно было в ТурбоС? Там вроде где-то довольно недалеко от .obj уже были бинарники с записью в конкретные железные порты.
                                                                              +2
                                                                              не так уж и близко, там через int 21h писалось, а 21h уже dos обрабатывал, который затем передавал управление в bios через int 10h
                                                                                +2
                                                                                Был еще вариант с прямой записью в видео память по адресам 0B800h:0000h, короче не уже и не придумаешь.
                                                                              0
                                                                              ладно хоть не было раскрутки с самого запуска приложения, там больше наслоений абстракций
                                                                                0
                                                                                а если еще вспомнить про отладку и ее пляски вокруг псевдорежимов процессора — вообще застрелиться можно…
                                                                                как сейчас помню бессонную неделю студенчества когда программа «модифицировала» свои ресурсные части и отладчик сходил с ума — приходилось «отлаживать» записывая все в лог
                                                                                0
                                                                                А можно ссылочки на исходные коды и объяснение того, как вы их находили?
                                                                                +1
                                                                                А почему в примере printf, а не puts?! Я прекрасно понимаю, что для данной статьи это не важно, а gcc вообще вместо подобного printf поставит puts, но сам факт того, что hello world пишут с использованием printf, заставляет новичков считать, что printf — это что-то нормальное, хотя в 99% случаев уже на стадии компиляции известно, что куда подставлять надо и парсить строку в real time уж точно никакого смысла нет. Printf пора уже сделать deprecated.
                                                                                  0

                                                                                  Эм, что? Почему-то мне кажется, что, может быть, в 50%, но никак не в 99%.


                                                                                  И, собственно, чем же вы предлагаете делать форматированный вывод в реалтайме, если вдруг стало очень надо?

                                                                                    +1

                                                                                    Согласен, везде пихают свой printf, хотя во многих случаях можно было обойтись обычным puts/fputs. Но зачем его депрекейтить, чем тогда форматировать в реальном времени?

                                                                                      0
                                                                                      Например, потому, что многих выбешивает запоминать, что puts() добавляет финальный \n, а fputs() — нет. И даже не просто запоминать, а вовремя вспоминать при замене stdout на файл или наоборот.
                                                                                      printf() в этом плане устойчивее, поведение всегда одинаково, а дополнительный скан на '%' стоит доли копеек по сравнению даже с переходом в ядро.
                                                                                        0
                                                                                        Не по сравнению «даже с походом в ядро». Поход в ядро — это не дорого. Это очень дорого. Сотни тактов. За которые суперскаляр может исполнить тысячи операций.

                                                                                        На этом микроядра погорели. Современная архитектура, вообще очень плохо «ложится» на современные стили написания программ. Что довольно грустно.

                                                                                        Так-то хорошо было бы жить в мире где то, что считается «хорошим стилем» было бы не только «красиво», но и «эффективно»… Но уж где живём — там и живём…
                                                                                          0
                                                                                          А попытки исправить это были? И если да, то почему они провалились?
                                                                                            0
                                                                                            Ну дык эта ж проблема не на ровном месте появилась. Просто переход через границу привилегий — это реально сложная задача. Нужно много чего сделать при переходе «туда» и «обратно».

                                                                                            И попытки ускорить не провалились. От тысяч тактов мы перешли примерно до 300-400 тактов. А последующие улучшиния довели это время до сотни примерно.

                                                                                            Проблема в другом. На каком-нибудь 80286 — «поход в ядро» тоже под сотню тактов занимает… но там и вызов функции — тактов 20-30. А у современного процессора это получается сделать за 5-6.

                                                                                            То есть проблема не в том, что кто-то замедлил «поход в ядро». Проблема в том, что «обычные» операции стали сильно быстрее работаеть.

                                                                                            P.S. Кстати очень хорошо видно почему те же люди, которые устроили все эти индирекции в GLibC грязно ругают тех, кто поверх этого десять слоёв абстракции навесил — они абсолютно правы. Потому что «навесить» 3-4 «быстрых» вызова по 5-10 тактов и за счёт этого получить вдвое меньше системых вызовов — это таки очень-таки неплохая экономия. Один системный вызов как 20, а то и 30 вызовов функций стоит. А вот если вы добавляете к цепочке функций ещё один слой и не получаете уменьшения количество вызовов в каком-то месте… то это в чистом виде замедление.
                                                                                      +1

                                                                                      До чего же было приятно читать: все разобрано шаг за шагом, компактно, с соответствующими листингами, объяснениями и т.д.
                                                                                      Большое спасибо за статью (и перевод)!

                                                                                        0
                                                                                        Вот и неоднозначная получается штука. Сам язык Си простой, за что его всегда хвалят противники ООП и прочего синтаксического сахара. А в итоге после всех макросов получается такая нечитабельная лапша, что невольно хочется развидеть это все. Я понимаю что этому всему лет больше чем мне и тогда были лихие годы писали как могли… Но наверное что-то должно поменяться в будущем, пока не осталось человек пять которые понимают как оно работает.
                                                                                          0

                                                                                          Ещё раз — это свойство не языка, а конкретного компилятора в конкретных условиях и с применением конкретных версий конкретных библиотек. Всей этой лапши в других условиях может просто не быть. Но может быт другая. Впрочем — в каком языке тот же функционал будет сделан проще?


                                                                                          А насчёт простоты Си — сейчас придет khim и вам расскажет, что он об этом думает )))

                                                                                          0
                                                                                          То чувство, когда ты понимаешь, наверное не всегда стоит знать как оно работает. Теперь при каждом printf будет переклинивать секунд на 10 воспоминаниями этого поста…
                                                                                            0
                                                                                            В helloworld Docker'a решили пропустить первые 6 пунктов. 1 системный вызов, красота.
                                                                                              +1
                                                                                              Они просто не хотели тащить за собой огромную libc. И в принципе их можно понять.
                                                                                              0

                                                                                              Вообще я думаю, поправьте меня, что все дело в том, что функция printf исходно нужна для форматированного вывода, а не для вывода константных строк (хотя их она тоже может выводить). И ее использование в этом примере просто является плохой практикой, которая пошла в массы. И когда массы видят такую вот сложную замену на puts и далее по тексту — их выворачивает. Если бы сразу было честно написано puts(string), либо не менее честно printf("%s",string), то не было бы этих проблем. Более того, в первой книге по Си, которую я читал в универе, годов так 80-х (переводная) хеллоуворлд был вообще с putchar и циклом, видимо чтобы сразу отбить охоту писать на Си. Вот как надо делать!

                                                                                                0
                                                                                                [мимо]
                                                                                                0
                                                                                                Все же надо заметить что аналог puts/printf есть в стандартной библиотеке практически любого языка. И в большинстве библиотек тоже будет навернуто несколько слоев абстракции перед вызовом syscall конкретной платформы на конкретной архитектуре. А уж что там ядро потом делает — от языка вообще никак не зависит.
                                                                                                  0
                                                                                                  Забавно, что GCC меняет на puts даже в режиме -O0, а C++ компилятор той же версии не меняет даже при -O3. Наверно где-то в глубинах стандартов можно найти ответ, но лень…
                                                                                                    0
                                                                                                    У меня меняет. И даже G++ 4.1 (самый древний, какой есть на gotbolt).

                                                                                                    Может вы что-то другое пишите? Например если написать return printf(«Hello World!\n»); — то он, разумеется, перестаёт менять…
                                                                                                      0
                                                                                                      o_O а ща меняет, хотя всегда было
                                                                                                      int main() {
                                                                                                      printf(«Hello World!\n»);
                                                                                                      }
                                                                                                      Понятно, что что-то менялось, но вроде копипаст только чистый был.
                                                                                                    +1
                                                                                                    До исполнения кода main(...) еще много чего происходит.
                                                                                                    Это гораздо интереснее, если честно :)
                                                                                                    Но очень платформоспецифично.

                                                                                                    Only users with full accounts can post comments. Log in, please.