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

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

Комментарии (136)


  1. MechanicZelenyy
    17.06.2019 23:37
    +1

    И эти люди ругают нас за наворачивание абстракции над абстракцией.


    1. netch80
      18.06.2019 07:41

      Так тут всё вполне просто, никаких наворотов.


    1. namikiri
      18.06.2019 10:00
      +1

      В том и дело, что в системе итак достаточно абстракций, хватит плодить ещё больше.


    1. khim
      18.06.2019 19:08

      Проблема не в том, что мы порождаем абстракции, а в том, что мы порождаем абстракции для решения проблем, уже решённых существующими абстракциями! Вот это — настоящий перпетум мобиль, с которым нужно-таки бороться.


  1. MagisterLudi
    18.06.2019 00:34

    Я всегда подозревал, что-то не то с этими сями…

    Поэтому:
    image


    1. Dmitri-D
      18.06.2019 03:27
      +2

      Собственно, «C» закончился вызовом библиотечной функции.
      Бейсик, думаете сможет сделать что-то лучше?


      1. IronHead
        18.06.2019 08:41

        Это был сарказм


      1. dedalqq
        18.06.2019 21:07
        +1

        Вообще, если говорить о урощении, то можно же напрямую дернуть системный вызов 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);
        }
        

        И компилится и работает без каких либо проблем.


        1. ooprizrakoo
          18.06.2019 21:32
          +2

          Нужны бенчи и тесты :) по самому быстрому выполнению хелловорлда :)


        1. alexeyknyshev
          18.06.2019 23:09

          Компилируются и сегфолтился )

          int main;


          Просто забавный оффтоп


          1. MaxVetrov
            18.06.2019 23:51

            (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


            1. MacIn
              19.06.2019 10:50

              add %al,(%rax)

              Т.е. оно компилится в нули.


              1. MaxVetrov
                19.06.2019 15:49

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


    1. staticlab
      18.06.2019 09:35

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


      1. IronHead
        18.06.2019 09:38

        Чтобы можно было написать GOTO 10


        1. MaxVetrov
          18.06.2019 11:52

          Или GOSUB 10 :)


          1. DMGarikk
            18.06.2019 13:30

            resume тогда будет нужен еще


            1. MaxVetrov
              18.06.2019 13:39

              1. DMGarikk
                18.06.2019 13:51

                Это от диалекта зависит, resume тоже бывает



          1. staticlab
            18.06.2019 16:18

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


            1. MaxVetrov
              18.06.2019 16:36

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


        1. MacIn
          18.06.2019 20:09

          А зачем писать GOTO 10, если можно написать

          DO
          ..
          LOOP WHILE TRUE


          или
          DO WHILE TRUE
          ..
          LOOP


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


          1. MaxVetrov
            18.06.2019 20:13

            1. MacIn
              18.06.2019 21:17

              В таком случае — да, но просто метка 10, как традиционно метка первой строки, не кажется подходящей для вашей интерпретации. Скорее, изначально имелся в виду бесконечный цикл в классическом 10 print… goto 10.
              А для выхода из цикла можно так же текстовую метку использовать.


              1. MaxVetrov
                18.06.2019 21:22

                :) Ну тогда можно просто — 10 GOTO 10
                И только исключение поможет.


        1. ksr123
          18.06.2019 22:12

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


    1. melodictsk
      18.06.2019 21:08

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

      Зачем заставлять писать на бэйские человека для которого это не первый язык программирования?


  1. Dmitri-D
    18.06.2019 03:26
    +1

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


  1. HEKOT
    18.06.2019 06:59

    Спасибо, я теперь могу совершенно чётко сформулировать, за что я люблю embedded…


    1. osmanpasha
      18.06.2019 08:49
      +3

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


      1. HEKOT
        18.06.2019 15:09

        Если он отладочный, то пофиг. А другого printf в embedded просто нет.Хотя нет, отладочного тоже нет. :)
        Недавно на эту тему на linkedin:
        — Чем отличается C от Embedded C?
        — Нет printf?
        — Нет malloc!


        1. alexey_public
          18.06.2019 15:43

          А вот я бы не стал так утверждать, ибо регулярно использую sprintf :-)
          Но да я знаю что он за собой тянет, тем не менее писать свой вариант sprintf еще более накладно. А нужно для того же вебсервера регулярно (и не только его, запись в файлы, общение с gsm-модулями и прочим, прочим). Да, можно и иначе сделать, но тогда придется что-то заметно урезать. Пока хватает ресурсов — применение sprintf весьма оправданно. ИМХО конечно. Например в высоконагруженном месте печать float сделал по собственной схеме, что позволило существенно ускорить процесс.


          1. masai
            18.06.2019 15:56

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


            1. alexey_public
              18.06.2019 17:06

              Вы абсолютно правы. Но практически у меня бывали и исключения, хотя вот вспоминаешь и думаешь — а не вернуться ли и не переделать ли :-)


        1. tmin10
          18.06.2019 16:36

          Хм, надвно наблюдал терминал оплаты сбера с ошибкой malloc, хотя это тоже должен быть embedded…


          1. HEKOT
            18.06.2019 17:32

            В первую очередь, это Сбер :D
            Вообще, это не совсем эмбеддед или совсем не ембеддед.
            Ещё ~100 лет назад в банкоматах стояла OS/2


            1. tmin10
              18.06.2019 17:52
              +2

              Не совсем верно написал.

              Я про такую железку говорил
              image


              1. talbot
                18.06.2019 18:54

                Так то не Сбер, а Верифон, с его прошивками. Да и там такой себе эмбеддед вроде, это довольно мощная железка уже. Вот например чип банковской карты тоже эмбеддед, в котором вполне себе Java приложение, да ещё и не одно.


                1. khim
                  18.06.2019 19:11

                  Вот например чип банковской карты тоже эмбеддед, в котором вполне себе Java приложение, да ещё и не одно.
                  Там такая себе Java, спцфцкая. Без GC и free — отличный подход к выделению памяти! Malloc там, фактически, аналог Sbrk…


    1. mikelavr
      18.06.2019 12:32

      За то, что весь этот путь информации хоть куда нибудь наружу надо проделывать самостоятельно? Даже если устройством показа пользователю является осциллограф на ножке GPIO.


      1. HEKOT
        18.06.2019 16:20

        Это было увлекательное занятие программировать 24-процессоную DSP систему реального времени, в которой к каждому процессору подключено по одному светодиоду. Процессоров было больше, 24 было только у меня. А наши мужики ещё умудрились найти багу в проприетарной ОС!

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


        1. sumanai
          18.06.2019 18:17

          Не стОит недооценивать осциллограф!

          Всё ждал, пока они там тетрис запустят.


          1. HEKOT
            18.06.2019 19:04
            +1

            Можно и тетрис.
            Вообще, есть такой жанр творчества: генерируется звуковой файл, к аудиовыходу компа подключается скоп и нажимается Play…


    1. DrGluck07
      18.06.2019 14:17

      Да-да, старый добрый эмбеддед. Сейчас как раз сижу с Code Composer Studio 9 и пытаюсь понять почему printf в консоль не гадит, хотя два дня назад вполне успешно это делал. Это я уже молчу, что нужно было специфически поставить опции на странице Debug, иначе отладчик вообще отваливается на попытке загрузить программу в процессор.


  1. sim2q
    18.06.2019 07:25

    Когда то очень давно, выдрал из glibc одну функцию и таскаю её уже страшно посчитать сколько лет уже. Но точно помню, что когда выдирал — такого кошмара там точно ещё не было:)


    1. salas
      18.06.2019 17:54

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


  1. WinPooh73
    18.06.2019 08:44
    +2

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


    1. masai
      18.06.2019 14:42
      +1

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


      1. vk000
        18.06.2019 21:12

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


  1. Massacre
    18.06.2019 09:49

    Так здесь 2-10 к ОС относится, компилируйте под DOS :)

    … не через gcc, разумеется.


    1. Whuthering
      18.06.2019 12:01

      компилируйте под DOS :)
      … не через gcc, разумеется.
      а почему бы и нет-то, DJGPP же есть…


      1. Massacre
        18.06.2019 13:46

        DJGPP это про защищённый режим и DPMI, абстракций там сравнимо будет, по идее.


  1. mikkoljcov
    18.06.2019 10:13

    В виндовом gcc замена printf на puts не происходит:

    call _printf

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


    1. pvl_1
      18.06.2019 12:42

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

      Исполняется именно то, что написано. Это основное требование к компиляторам. Просто в данном конкретном случае компилятор знает, что puts делает ровно то же самое, что и printf, но гораздо быстрее.


      1. mikkoljcov
        18.06.2019 12:59

        Скажем так: в качестве примера первой «самой простой» программы Керниган взял самую сложную и неоднозначную функцию, которая к тому же не выполняется. Вот такой Hello world.


        1. Dim0v
          18.06.2019 13:50

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


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


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


          1. mikkoljcov
            18.06.2019 14:00

            Ну так и написал бы puts(). Но нет, хотелось козырнуть printf(), которая тут вообще, оказывается, не стояла. Ричи, кстати, должен был знать — он же компилятор точил.


            1. GLeBaTi
              18.06.2019 14:03

              Возможно в примерах часто был нужен именно форматированный вывод. Наверное поэтому и использовал printf.


              1. mikkoljcov
                18.06.2019 14:25

                Сам Керниган признаётся во вступлении, что «не копенгаген» в ассемблере, оно и видно. А Hello world зря взяли в «самые простые» программы. У Строструпа она выглядит совсем кошмарно — cout << «Hello world». Этим надо заканчивать курс СПП, а не начинать.


                1. Ddnn
                  18.06.2019 20:35

                  В свое время форматные строки для printf и scanf стали причиной, по которой я выбрал учить C++, а не С (для С взял книжку не K&R, что, конечно, было ошибкой).

                  Но вообще, в последнем издании Страуструпа, где он учит программированию на примере С++ (http://www.stroustrup.com/Programming/), он явно не ставит себе цели сразу объяснить, как оно все работает внутри — а наоборот, призывает использовать удобные абстракции, так что cout здесь вполне себе адекватно выглядит.


                  1. lorc
                    18.06.2019 21:01
                    +1

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

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


                    1. Ddnn
                      18.06.2019 21:24

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


                    1. MaxVetrov
                      18.06.2019 21:33

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

                      Можно через форматирование строки.


                      1. lorc
                        18.06.2019 21:43

                        Есть же какое-то .)

                        Так я ж и написал про длинную колбасу из модификаторов стримов: ширину задай, заполняющий символ задай, основание системы счисления задай. А потом верни все обратно — т.е. снова задай, но уже значения по умолчанию.


                        1. MaxVetrov
                          18.06.2019 21:55

                          Cтроки с форматированием могут быть опасными.


                          1. lorc
                            18.06.2019 22:06

                            Я надеюсь что все более-менее опытные разработчики знают что в строке форматирования должна быть константа.


                            1. MaxVetrov
                              18.06.2019 22:58

                              Да, все верно.


                        1. netch80
                          19.06.2019 11:03

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

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


                      1. khim
                        18.06.2019 21:52
                        +2

                        Ну на эту тему Хорстман обстебался в своём 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.)


                        1. MaxVetrov
                          18.06.2019 22:05

                          Развитие идет по спирали :)

                          PS. Оригинальная статья(Hell World) у меня не открывается без прокси.


                          1. khim
                            18.06.2019 23:28

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

                            Особенно «Note the lack of semi-colon. Improvement!»


                            1. MaxVetrov
                              19.06.2019 00:01

                              Да, тогда можно было подумать, что это тупик :)


            1. Dim0v
              18.06.2019 14:28

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


              1. mikkoljcov
                18.06.2019 14:33

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


                1. Dim0v
                  18.06.2019 14:37

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


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


                  1. mikkoljcov
                    18.06.2019 14:40

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

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


                    1. Zoomerman
                      18.06.2019 15:02

                      3. тем, что она тупо зря гоняет процессор/препроцессор в поисках подстановок %i, которых там нет

                      Так она и не гоняет — компилятор направляет по пути наименьшего сопротивления — подменяет на puts.
                      А вот если бы в форматной строке были переменные — таки да, погнало бы по большому кругу и ассемблерный код был бы значительно сложнее.
                      Просто одна из моделей оптимизации компилируемого кода.


                      1. mikkoljcov
                        18.06.2019 15:11

                        Товарищ выше намекает, что в славные 70-е компилятор не подменял. А виндовый gcc и сейчас не подменяет. Но это не суть, а на сдачу. Суть в первых двух пунктах.


                        1. Zoomerman
                          18.06.2019 16:00

                          Первые 2 пункта верны на все 100.
                          Помнится, обучение сям начиналось с puts/gets, и только через пару-тройку месяцев переходили к комбайнам printf/scanf.


                          1. mikkoljcov
                            18.06.2019 16:14

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


                    1. Dim0v
                      18.06.2019 15:28

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


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


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

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


                      1. force
                        18.06.2019 18:29

                        В своё время было очень много взломов из-за того, что люди привыкали использовать printf где ни попадя вместо puts. В конце концов получался printf вывода пользователя или неожиданно вылезал в строке %, и всё падало. Т.е. в данном случае новичков сразу учат плохому, которое потом надо будет выправлять.


                        1. pvl_1
                          19.06.2019 17:56

                          Лучше выправлять некоторую небезопасность, заставляя уже наученного работника использовать безопасные версии printf, чем учить его всему форматированному выводу.


                  1. drapass
                    18.06.2019 16:15

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


                  1. Eswcvlad
                    18.06.2019 21:13

                    Ну если вы запихнете при запуске приложения в LD_PRELOAD библиотеку, которая экспортирует puts, но не printf, то поведение при такой оптимизации поменяется. Понятно, что на практике это маловероятно, но все же.

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


                    1. khim
                      18.06.2019 21:54
                      +1

                      Или их такое переопределение считается как UB?
                      Их поведение описано в стандарте. Если вы его меняете, то получаете среду несовместимую со стандартом — и тут уже говорить о том, UB это или не UB смысла не имеет.


                    1. netch80
                      19.06.2019 10:57
                      +1

                      > Откуда gcc может быть уверен, что это именно printf и puts из стандартной библиотеки?

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

                      Там, где это не так, можно собирать для freestanding (так делают, например, во FreeBSD для ядра, rtld и ещё немного специфических компонентов).


              1. mikkoljcov
                18.06.2019 14:59
                -1

                Вы, кстати, зря не считаете компилятор частью языка. Фортран — первый компилируемый язык — вообще включает компиляцию в название. То, что это часть сменная, не значит, что это не часть.


                1. Dim0v
                  18.06.2019 15:08
                  +1

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


                  1. mikkoljcov
                    18.06.2019 15:16
                    -1

                    Поменяется компилятор — поменяются правила языка. Все эти стандарты — просто бумажки. Рулит компилятор, поэтому разделять его с языком — ошибка, точнее, идеализация. То же самое с HTML — есть поддержка в браузере (интерпретаторе) — есть в языке. Нет — гуляй с пляжа.


                    1. khim
                      18.06.2019 19:24
                      +2

                      Если чего-то есть в спеке, но нет в компиляторе — то это повод зафайлить бегу и компилятор (или, гораздо реже, спеку) исправят… а вот если чего-то было в компиляторе, а потом пропало… то это, в лучшем случае, вызовет реакцию типа «как-же всё-таки хорошо, что вот это вот всё, что вы написали — это не наша проблема».


                    1. netricks
                      19.06.2019 11:56

                      Стандарт языка придумали не просто так.

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

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

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


                      1. mikkoljcov
                        19.06.2019 12:24
                        -1

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

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


                        1. netricks
                          19.06.2019 12:39

                          Язык си может быть обработан интерпретатором.
                          И это не будет нарушением стандарта.

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

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

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

                          И это никак не повлияет на сам язык.


                          1. mikkoljcov
                            19.06.2019 12:55
                            -1

                            Причина минусов в том, что можно не думать, а действовать.

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

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


                        1. khim
                          19.06.2019 14:34

                          Я как раз утверждал, что язык и компилятор — единое целое.
                          Что, собственно, и является грубой ошибкой. У каждого компилятора есть куча особенностей, которые использовать нельзя.

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

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

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

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

                          В других языках (скажем Java) прилагаются очень серьёзные усилия к тому, чтобы программист не смог «усмотреть» в компиляторе чего-то, чего нет в спеке, в C/C++ — такие усилия не прилагаются. Правильная программа, соответствующая спеке — обязана работать, неправильная — тоже может работать, но что она, при этом будет делать — разработчиков не волнует от слова совсем. И это принципиальная позиция разработчиков. Потому в Java можно сказать, почти не покривив душой, что компилятор и язык — это одно и то же, но в C/C++ — нельзя.


  1. ice2heart
    18.06.2019 10:19

    А если добавить ещё вывод времени в лог…


  1. netricks
    18.06.2019 11:04

    Это прекрасно. Мне бы эту статью лет десять надад.


    1. sumanai
      18.06.2019 18:23

      10 не выйдет, только 5 с лишним

      Hello World Analysis
      11 Oct 2013


  1. Gymmasssorla
    18.06.2019 11:26

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


    1. netricks
      18.06.2019 11:41
      -1

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

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

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

      Насколько такая цепочка оправдана… Вопрос открытый.


      1. GarryC
        18.06.2019 11:43

        На это наверняка были веские основания, к сожалению, те люди, которые их знали, на смогут поделится с нами по веским причинам — они все эти основания забыли.
        Так что единственное доставшееся нам объяснение: «У нас так принято».


        1. netricks
          18.06.2019 11:46
          +1

          Это решение родилось в процессе эволюции системы. Тут как по Энштейну. Настолько просто, насколько возможно, но ничуть не проще.


          1. cubit
            18.06.2019 14:20

            Поправьте: «Эйнштейн».


            1. netricks
              18.06.2019 16:25

              Время вышло… Думаю, Альберт Великий не обиделся.


              1. MaxVetrov
                18.06.2019 16:41

                Да, Альберт Каменный или Каменев, не обиделся.)


          1. alsii
            18.06.2019 14:51

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


            1. netricks
              18.06.2019 16:31

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


              1. alsii
                18.06.2019 16:40

                Неэфективные решения эволюционно нестабильны

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


        1. netricks
          18.06.2019 11:52

          На самом деле, никто не мешает написать myprintf напрямую через write. Это будет прекрасно работать. Можно даже обойтись без glibc совсем. Но glibc предлагает решение среднее по больнице, которое подходит в 98% случаев. И да, оно зачастую избыточно, но работает как часы…


  1. nihonam
    18.06.2019 13:53

    а как оно было в ТурбоС? Там вроде где-то довольно недалеко от .obj уже были бинарники с записью в конкретные железные порты.


    1. tbl
      18.06.2019 13:58

      не так уж и близко, там через int 21h писалось, а 21h уже dos обрабатывал, который затем передавал управление в bios через int 10h


      1. Alexus819
        18.06.2019 15:15

        Был еще вариант с прямой записью в видео память по адресам 0B800h:0000h, короче не уже и не придумаешь.


  1. tbl
    18.06.2019 13:54

    ладно хоть не было раскрутки с самого запуска приложения, там больше наслоений абстракций


    1. toxicdream
      18.06.2019 14:16

      а если еще вспомнить про отладку и ее пляски вокруг псевдорежимов процессора — вообще застрелиться можно…
      как сейчас помню бессонную неделю студенчества когда программа «модифицировала» свои ресурсные части и отладчик сходил с ума — приходилось «отлаживать» записывая все в лог


  1. ganqqwerty
    18.06.2019 14:28

    А можно ссылочки на исходные коды и объяснение того, как вы их находили?



  1. Alexey_Alive
    18.06.2019 16:17

    А почему в примере printf, а не puts?! Я прекрасно понимаю, что для данной статьи это не важно, а gcc вообще вместо подобного printf поставит puts, но сам факт того, что hello world пишут с использованием printf, заставляет новичков считать, что printf — это что-то нормальное, хотя в 99% случаев уже на стадии компиляции известно, что куда подставлять надо и парсить строку в real time уж точно никакого смысла нет. Printf пора уже сделать deprecated.


    1. hhba
      18.06.2019 19:27

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


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


    1. Gymmasssorla
      18.06.2019 20:04
      +1

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


    1. netch80
      19.06.2019 10:52

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


      1. khim
        19.06.2019 17:32

        Не по сравнению «даже с походом в ядро». Поход в ядро — это не дорого. Это очень дорого. Сотни тактов. За которые суперскаляр может исполнить тысячи операций.

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

        Так-то хорошо было бы жить в мире где то, что считается «хорошим стилем» было бы не только «красиво», но и «эффективно»… Но уж где живём — там и живём…


  1. informatik01
    18.06.2019 16:17

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


  1. zim32
    18.06.2019 16:27
    -1

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


    1. hhba
      18.06.2019 19:30

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


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


  1. ua30
    18.06.2019 17:08

    То чувство, когда ты понимаешь, наверное не всегда стоит знать как оно работает. Теперь при каждом printf будет переклинивать секунд на 10 воспоминаниями этого поста…


  1. OlegSchwann
    18.06.2019 18:20

    В helloworld Docker'a решили пропустить первые 6 пунктов. 1 системный вызов, красота.


    1. lorc
      18.06.2019 19:42

      Они просто не хотели тащить за собой огромную libc. И в принципе их можно понять.


  1. hhba
    18.06.2019 19:38

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


    1. lorc
      18.06.2019 19:51

      [мимо]


  1. lorc
    18.06.2019 19:51

    Все же надо заметить что аналог puts/printf есть в стандартной библиотеке практически любого языка. И в большинстве библиотек тоже будет навернуто несколько слоев абстракции перед вызовом syscall конкретной платформы на конкретной архитектуре. А уж что там ядро потом делает — от языка вообще никак не зависит.


  1. rogoz
    18.06.2019 23:08

    Забавно, что GCC меняет на puts даже в режиме -O0, а C++ компилятор той же версии не меняет даже при -O3. Наверно где-то в глубинах стандартов можно найти ответ, но лень…


    1. khim
      18.06.2019 23:34

      У меня меняет. И даже G++ 4.1 (самый древний, какой есть на gotbolt).

      Может вы что-то другое пишите? Например если написать return printf(«Hello World!\n»); — то он, разумеется, перестаёт менять…


      1. rogoz
        19.06.2019 01:04

        o_O а ща меняет, хотя всегда было
        int main() {
        printf(«Hello World!\n»);
        }
        Понятно, что что-то менялось, но вроде копипаст только чистый был.


  1. kunix
    19.06.2019 15:03

    До исполнения кода main(...) еще много чего происходит.
    Это гораздо интереснее, если честно :)
    Но очень платформоспецифично.