Занимательная археология. Или PVS-Studio проверяет Microsoft Word 1.1a

    Череп единорога
    Недавно компания Microsoft сделала подарок всем программистам, которые хотят покопаться в чем-то интересном. Microsoft открыли исходный код MS-DOS v 1.1, v 2.0 и Word for Windows 1.1a. Операционная система MS-DOS написана на ассемблере, и к ней анализатор не применим. А вот Word написан на языке Си. Исходным кодам Word 1.1a почти 25 лет, однако нам кое-как удалось их проверить. Конечно никакой практической ценности в этой проверке нет. Just for fun.

    Где поживиться исходниками


    Возможно, многим будет интересна не сколько эта статья, а сам факт, что можно скачать исходные коды MS-DOS v 1.1, v 2.0 и Word for Windows 1.1a. Тем, кому интересно самим покопаться в исходных кодах, отправляю к первоисточнику.

    Пресс-релиз: Computer History Museum Makes Historic MS-DOS and Word for Windows Source Code Available to the Public.

    Проверка Word 1.1a


    Рисунок 1. Word for Windows 1.1a.

    Рисунок 1. Word for Windows 1.1a.

    Word for Windows 1.1a был выпущен в 1990 году. 25 марта 2014 код этого продукта стал доступен публике. Word был и остаётся флагманским продуктом компании Microsoft. Мне и многим другим интересно посмотреть на внутренности программного продукта, который так сильно поспособствовал коммерческим успехам компании Microsoft.

    Я решил проверить код Word 1.1a с помощью нашего инструмента PVS-Studio. Это статический анализатор Си/Си++ кода. Естественно, это не так просто. Анализатор рассчитан на работу с проектами, разрабатываемыми как минимум в Visual Studio 2005. А сейчас предо мной исходники на языке Си, которым более 20 лет. Можно сказать, что это доисторические времена. По крайней мере, тогда не существовало стандарта языка Си. Каждый компилятор был сам по себе. К счастью, в исходных кодах Word 1.1a не оказалось каких-то необычных моментов и использования большого количества нестандартных расширений компилятора.

    Для анализа необходимы препроцессированные файлы (*.i). Имея препроцессированные файлы, можно воспользоваться инструментом PVS-Studio Standalone. С его помощью можно выполнить анализ и изучить диагностические сообщения. Конечно, анализатор не рассчитан на анализ 16-битных программ. Но этих результатов анализа будет вполне достаточно для удовлетворения любопытства. Внимательно анализировать проект 24 летней давности нет никакого практического смысла.

    Итак, основная загвоздка состояла в том, как получить препроцессированные файлы. Я попросил своего коллегу поколдовать в этом направлении. Он подошёл к решению весьма творчески. Он выполнил препроцессирование с помощью GCC 4.8.1. Вряд ли кто-то ещё так издевался над исходниками Word 1.1. Использовать GCC — надо ведь было такое придумать. Фантазёр.

    Самое интересное, что вышло вполне удачно. Была написана маленькая утилита, которая запускала препроцессирование с помощью GCC 4.8.1 на каждый файл из директории, в которой он лежал. По мере вывода ошибок, связанных с включением заголовочных файлов, в параметры запуска добавлялись ключи -I с путём до нужных файлов. Парочка ненайденных заголовочных файлов были созданы пустыми. Все остальные проблемы раскрытия #include были связаны с включением ресурсов, поэтому были закомментированы. При препроцессировании определялся макрос WIN, т.к. в коде есть ветка для WIN и MAC.

    Дальше в дело вступил PVS-Studio Standalone и ваш покорный слуга. Я выписал подозрительные фрагменты кода и готов вам их показать. Но вначале ещё кое что о проекте.

    Разное о коде Word 1.1a



    Самые сложные функции


    Самая большая цикломатическая сложность у следующих функций:
    1. CursUpDown — 219;
    2. FIdle — 192;
    3. CmdDrCurs1 — 142.

    #ifdef WIN23


    Просматривая исходные коды и встретив "#ifdef WIN23", я заулыбался. И даже выписал это место. Я подумал, что это опечатка и должно быть написано #ifdef WIN32.

    Когда я увидел WIN23 второй раз я засомневался. А потом вдруг осознал, что я смотрю исходники 24 летней давности. WIN23 означает версию Windows 2.3.

    Суровые времена


    В коде мне попалась вот такая интересная строка.
    Assert((1 > 0) == 1);

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

    Конечно, если считать K&R стандартом, то по идее условие ((1 > 0) == 1) всегда выполняется. Но K&R это был лишь стандарт де-факто и не более. Это проверка на адекватность компилятора.

    Результаты проверки


    Теперь поговорим о подозрительных местах, найденных мною в коде. Думаю, ради этого вы и читаете эту статью. Приступим.

    Бесконечный цикл


    void GetNameElk(elk, stOut)
    ELK elk;
    unsigned char *stOut;
    {
      unsigned char *stElk = &rgchElkNames[mpelkichName[elk]];
      unsigned cch = stElk[0] + 1;
    
      while (--cch >= 0)
        *stOut++ = *stElk++;
    }

    Предупреждение PVS-Studio: V547 Expression '-- cch >= 0' is always true. Unsigned type value is always >= 0. mergeelx.c 1188

    Цикл «while (--cch >= 0)» никогда не остановится. Переменная 'cch' имеет тип unsigned. Значит, сколько не уменьшай эту переменную, она всегда останется >= 0.

    Выход за границу массива из-за опечатки


    uns rgwSpare0 [5];
    
    DumpHeader()
    {
      ....
      printUns ("rgwSpare0[0]   = ", Fib.rgwSpare0[5], 0, 0, fTrue);
      printUns ("rgwSpare0[1]   = ", Fib.rgwSpare0[1], 1, 1, fTrue);
      printUns ("rgwSpare0[2]   = ", Fib.rgwSpare0[2], 0, 0, fTrue);
      printUns ("rgwSpare0[3]   = ", Fib.rgwSpare0[3], 1, 1, fTrue);
      printUns ("rgwSpare0[4]   = ", Fib.rgwSpare0[4], 2, 2, fTrue);
      ....
    }

    Предупреждение PVS-Studio: V557 Array overrun is possible. The '5' index is pointing beyond array bound. dnatfile.c 444

    Как-то так получилось, что в первой строке написано: Fib.rgwSpare0[5]. Это неправильно. В массиве всего 5 элементов, а значит максимальный индекс должен быть равен 4. Значение '5' это результат опечатки. По всей видимости в первой строке должен был использоваться нулевой индекс:
    printUns ("rgwSpare0[0]   = ", Fib.rgwSpare0[0], 0, 0, fTrue);

    Неинициализированная переменная


    FPrintSummaryInfo(doc, cpFirst, cpLim)
    int doc;
    CP cpFirst, cpLim;
    {
      int fRet = fFalse;
      int pgnFirst = vpgnFirst;
      int pgnLast = vpgnLast;
      int sectFirst = vsectFirst;
      int sectLast = sectLast;
      ....
    }

    Предупреждение PVS-Studio: V573 Uninitialized variable 'sectLast' was used. The variable was used to initialize itself. print2.c 599

    Переменная 'sectLast' присваивается сама себе:
    int sectLast = sectLast;

    Кажется, для инициализации должна была быть использована переменная 'vsectLast':
    int sectLast = vsectLast;

    Нашлось ещё одна идентичная ошибка. Видимо последствие Copy-Paste:

    V573 Uninitialized variable 'sectLast' was used. The variable was used to initialize itself. print2.c 719

    Неопределённое поведение


    CmdBitmap()
    {
      static int  iBitmap = 0;
      ....
      iBitmap = ++iBitmap % MAXBITMAP;
    }

    Предупреждение PVS-Studio: V567 Undefined behavior. The 'iBitmap' variable is modified while being used twice between sequence points. ddedit.c 107

    Не знаю, как к такому коду относились 20 лет назад. Но сейчас это считается хулиганством, так как приводит к неопределённому поведению.

    Аналогично:
    • V567 Undefined behavior. The 'iIcon' variable is modified while being used twice between sequence points. ddedit.c 132
    • V567 Undefined behavior. The 'iCursor' variable is modified while being used twice between sequence points. ddedit.c 150

    Неудачный вызов функции printf()


    ReadAndDumpLargeSttb(cb,err)
      int     cb;
      int     err;
    {
      ....
      printf("\n - %d strings were read, "
             "%d were expected (decimal numbers) -\n");
      ....
    }

    Предупреждение PVS-Studio: V576 Incorrect format. A different number of actual arguments is expected while calling 'printf' function. Expected: 3. Present: 1. dini.c 498

    Функция printf(), это функция с переменным количеством аргументов. Ей можно передать аргументы, а можно и не передать. Вот здесь про аргументы забыли, в результате чего будет распечатан мусор.

    Неинициализированные указатели


    В одной из вспомогательных утилит, которая имеется в составе исходников Word, можно встретить что-то вообще непонятное.
    main(argc, argv)
    int argc;
    char * argv [];
    {
      FILE * pfl;
      ....
      for (argi = 1; argi < argc; ++argi)
      {
        if (FWild(argv[argi]))
        {
          FEnumWild(argv[argi], FEWild, 0);
        }
        else
        {
          FEWild(argv[argi], 0);
        }
    
        fclose(pfl);
      }
      ....
    }

    Предупреждение PVS-Studio: V614 Uninitialized pointer 'pfl' used. Consider checking the first actual argument of the 'fclose' function. eldes.c 87

    Переменная 'pfl' не инициализируется до цикла и в самом цикле. Зато много раз вызывается функция fclose(pfl). Впрочем, всё это вполне могло успешно работать. Функция вернёт статус ошибки, и программа продолжит свою работу.

    А вот ещё одна опасная функция. Скорее всего, её вызов приведёт к аварийному завершению программы.
    FPathSpawn( rgsz )
    char *rgsz[];
    { /* puts the correct path at the beginning of rgsz[0]
         and calls FSpawnRgsz */
      char *rgsz0;
    
      strcpy(rgsz0, szToolsDir);
      strcat(rgsz0, "\\");
      strcat(rgsz0, rgsz[0]);
      return FSpawnRgsz(rgsz0, rgsz);
    }

    Предупреждение PVS-Studio: V614 Uninitialized pointer 'rgsz0' used. Consider checking the first actual argument of the 'strcpy' function. makeopus.c 961

    Указатель ' rgsz0' ничем не инициализируется. Это не мешает начать копировать в него строку.

    Опечатка в условии


    ....
    #define wkHdr    0x4000
    #define wkFtn    0x2000
    #define wkAtn    0x0008
    ....
    #define wkSDoc    (wkAtn+wkFtn+wkHdr)
    
    CMD CmdGoto (pcmb)
    CMB * pcmb;
    {
      ....
      int wk = PwwdWw(wwCur)->wk;
        if (wk | wkSDoc)
          NewCurWw((*hmwdCur)->wwUpper, fTrue);
      ....
    }

    Предупреждение PVS-Studio: V617 Consider inspecting the condition. The '(0x0008 + 0x2000 + 0x4000)' argument of the '|' bitwise operation contains a non-zero value. dlgmisc.c 409

    Условие (wk | wkSDoc) всегда истинно. На самом деле, здесь, скорее всего, хотели написать:
    if (wk & wkSDoc)

    В общем, перепутали оператор | и &.

    И под конец длинный, но простой пример


    int TmcCharacterLooks(pcmb)
    CMB * pcmb;
    {
      ....
      if (qps < 0)
      {
        pcab->wCharQpsSpacing = -qps;
        pcab->iCharIS = 2;
      }
      else  if (qps > 0)
      {
        pcab->iCharIS = 1;
      }
      else
      {
        pcab->iCharIS = 0;
      }
      ....
      if (hps < 0)
      {
        pcab->wCharHpsPos = -hps;
        pcab->iCharPos = 2;
      }
      else  if (hps > 0)
      {
        pcab->iCharPos = 1;
      }
      else
      {
        pcab->iCharPos = 1;
      }
      ....
    }

    Предупреждение PVS-Studio: V523 The 'then' statement is equivalent to the 'else' statement. dlglook1.c 873

    Когда работают с переменной 'qps', то записывают в 'pcab->iCharIS' следующие значения: 2, 1, 0.

    Аналогично работают с переменной 'hps'. Но при этом в переменную 'pcab->iCharPos' помещаются подозрительные числа: 2, 1, 1.

    Скорее всего, это опечатка. В самом конце, наверное, следовало использовать ноль.

    Заключение


    Найдено совсем немного странных мест. Причины две. Во-первых, код мне показался написан качественно и весьма понятно. Во-вторых, анализ был все-таки неполноценным. Учить же анализатор особенностям старого Си нет практической надобности.

    Надеюсь, я подарил вам несколько минут интересного чтения. Спасибо за внимание. И попробуйте анализатор PVS-Studio на своём коде.

    Эта статья на английском


    Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Archeology for Entertainment, or Checking Microsoft Word 1.1a with PVS-Studio.

    Прочитали статью и есть вопрос?
    Часто к нашим статьям задают одни и те же вопросы. Ответы на них мы собрали здесь: Ответы на вопросы читателей статей про PVS-Studio и CppCat, версия 2014. Пожалуйста, ознакомьтесь со списком.

    PVS-Studio

    256,99

    Ищем ошибки в C, C++ и C# на Windows, Linux, macOS

    Поделиться публикацией

    Похожие публикации

    Комментарии 49
      0
      Assert((1 > 0) == 1);

      Интересно, как выглядела эта строка до препроцессора
        +21
        Это и есть строка до препроцессора.
          +4
          Видимо проверка на соответствие компилятора стандарту. Видать нарывались…
            +6
            Извините, пожалуйста, за возможно глупый вопрос.
            Разве в стандартах тех лет определяется значение константы TRUE? У меня почему-то в голове отложилась концепция «0=FALSE, остальное — TRUE». Если построить транслятор, который будет выдавать результатом 1 > 0, скажем, 10 — разве это будет отклонением от стандарта?
              +10
              Вот именно это они, очевидно, и проверяли — видимо, там в последующем коде где-то можено было найти что-нибудь типа такого:
              if ((a>b + c>d + e>f) > 2) { ... }

              Ну, типа, проверили, что по крайней мере два условия из трёх выполнены.
                +3
                C89 3.3.8:
                Each of the operators < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to) shall yield 1 if the specified relation is true and 0 if it is false. The result has type int.
                  +1
                  Однако в первых компиляторах Си, с которыми я работал, true было равно -1. Ибо ассемблерное not 0 давало 0xFF/0xFFFF
                    0
                    Как вам уже ответили в соседней ветке — это лишь означает, что данный компилятор не был компилятором языка C в версии от 89 года. Word же, очевидно, полагался на этот пункт станадарта, и, для исключения сложно диагностируемых багов, проверял данное условие.
                0
                Раньше компилятор волен был использовать любые удобные ему числовые представления для логических констант. Напомню, что в древних компиляторах C типа bool не было.

                Похожая ситуация есть и сейчас. А именно, пустой указатель вовсе не обязан быть нулевым. Строго говоря, равенство пустого указателя 0 это не более чем соглашение на правах стандарта, которое, тем не менее, может отличаться от платформы к платформе. Для корректного лексического представления пустого указателя был введен литерал nullptr, на равенство которому следует проверять указатели.
                  +1
                  пустой указатель вовсе не обязан быть нулевым. Строго говоря, равенство пустого указателя 0 это не более чем соглашение на правах стандарта


                  c-faq.com/null/null2.html и вообще вся тема c-faq.com/null/ — для вас.
                    0
                    Я работал с компилятором Си, где NULL имел конкретный адрес, указывающий на перехватчик исключения. Так обрабатывались NPE.
                      0
                      Это означает лишь то, что компилятор не соответствует стандарту.
              +20
              Баги зарегали?
                +5
                Позняк метаться. Это уже в золотой версии.
                  +9
                  А критикал апдейт выпустить?
                    +18
                    это же теперь опен сорс, на гитхаб и пул реквест.
                +5
                Интересно, сколько в коде есть костылей, которые нейтрализуют действие вот этих косяков? И в каком месте что поломается, если эти косяки исправить.
                  +1
                  Возможно что и много. Где-то проскакивала статья про методологию разработки и исправления кода у этих товарищей, мол ни в коем случае не менять имеющийся код, только дописывать. Уж не помню из каких соображений это было и насколько можно было верить источнику, но… :)

                  P.S.: По моему, в той же статье писалось и о курьезном костыле в ядре ОС для работоспособности конкретной игры.
                    +6
                    Насколько я помню, Спольски писал о том, что не надо переписывать с нуля.

                    Про баг с СимСити:

                    www.joelonsoftware.com/articles/APIWar.html
                    first heard about this from one of the developers of the hit game SimCity, who told me that there was a critical bug in his application: it used memory right after freeing it, a major no-no that happened to work OK on DOS but would not work under Windows where memory that is freed is likely to be snatched up by another running application right away. The testers on the Windows team were going through various popular applications, testing them to make sure they worked OK, but SimCity kept crashing. They reported this to the Windows developers, who disassembled SimCity, stepped through it in a debugger, found the bug, and added special code that checked if SimCity was running, and if it did, ran the memory allocator in a special mode in which you could still use memory after freeing it.
                      0
                      Про то, что не надо переписывать код:
                      www.joelonsoftware.com/articles/fog0000000069.html

                      It's a bit smarmy of me to criticize them for waiting so long between releases. They didn't do it on purpose, now, did they?

                      Well, yes. They did. They did it by making the single worst strategic mistake that any software company can make:

                      They decided to rewrite the code from scratch.


                      Заметьте, что это позиция Джоэла, а не MS
                      +4
                      Я впервые услышал об этом от одного из разработчиков популярной игры SimCity, который поведал мне о критической ошибке в их программе: она использовала память сразу после ее освобождения. Главное табу, нарушение которого прощалось в DOS, но карается в Windows, где освобожденную память тут же стащит другое работающее приложение. Тестеры в команде разработки Windows протестировали множество популярных приложений, чтобы убедиться, что все работает без сбоев, но SymCity зависала. Они сообщили это разработчикам Windows, которые дизассемблировали SymCity, шаг за шагом в дебаггере найдя ошибку, и добавили специальный код, проверяющий наличие SymCity в памяти и запускающий распределитель памяти в специальном режиме, в котором SymCity разрешается использовать память после ее освобождения.


                      Отсюда.

                      P.S. Я всегда буду обновлять комментарии перед отправкой своего =(
                    +1
                    Интересно было бы увидеть комментарии разработчиков этого кода к данному анализу.
                      +8
                      «Как мы этот проект запускали… Как вспомню, так вздрогну! И как оно работает — сам не пойму! )»
                        +3
                        БиллГейтс-ИКак-тоТакЧерезХитро...jpg
                          +1
                          Ballmer Peak
                        +8
                        По секрету. Уже готова статья про проверку Unreal Engine. Есть интересное. Например, код для работы с Oculus Rift видимо ещё сыроват :)
                        void FOculusRiftHMD::PreRenderView_RenderThread(FSceneView& View)
                        {
                          ....
                          if (View.StereoPass == eSSP_LEFT_EYE ||
                              View.StereoPass == eSSP_LEFT_EYE)
                          ....
                        }
                        

                        Правда до следующего релиза PVS-Studio я не могу её опубликовать. Почему — станет ясно из статьи.

                        Это я для задабривания :) Ибо есть просьба к тем, кому нравятся наши заметки. Прошу давать в twitter ссылку на английский вариант статьи. И тут ещё можно подтолкнуть. Если тема пойдёт, глядишь, кто-то и из Microsoft комментарий где-то оставит.
                          +6
                          «Кто-то из MS» комментарий может оставить и здесь, нам не жалко :)
                            +12
                            Надо сделать спамбота, который будет проверять исходники в публичных репозиториях и автоматически писать в форумы возмущенные собщения о найденных ошибках.
                              +9
                              Одноглазый Джо не видит, в чем проблема.
                                +1
                                Могучий Ctrl-C Ctrl-V. Ну да, они же сказали, мол-де за отполированным продуктом — через 6 месяцев приходите. А статью ждём!
                                  0
                                  А вот и статья про UE4: habrahabr.ru/company/pvs-studio/blog/219245/
                                  +7
                                  Конечно это верх эгоизма, но не могли бы вы проверить один из популярных проектов «freeswitch»?
                                  Я думаю разработчики были бы очень рады, да и комьюнити у них хорошее
                                  +1
                                  >> Можно сказать, что это доисторические времена. По крайней мере, тогда не существовало стандарта языка Си.

                                  В 1990-м уже был ANSI C89. Насколько ему соответствует компилятор — это другой вопрос.

                                  Если честно, немного удивлен, что PVS не загнулся на far и near указателях, которые в то время очень часто встречались в 16-битном коде под интел.
                                    +9
                                    Если честно, немного удивлен, что PVS не загнулся на far и near указателях, которые в то время очень часто встречались в 16-битном коде под интел.


                                    Сами в шоке :-)
                                    +7
                                    Имена переменных — ад. PwwdWw.
                                      +10
                                      Человек заикался, а вы попрекаете :-)
                                        +20
                                        Шрифт Tms Rmn нрм
                                          +6
                                          Так и префиксы их системной венгерки не лыком шиты, навроде lpcszw :)
                                          +13
                                          Не планируете проверить Bitcoin/Litecoin?
                                          Интересно было бы почитать, какие ошибки там найдутся
                                            0
                                            Открывают код Apollo:
                                            apcmag.com/apollo-11-code-goes-open-source.htm
                                            Жаль, что там не C…
                                              0
                                              Есть у вас один недочет. Сейчас перечитал — заметил. В разделе «Неудачный вызов функции printf()».
                                              Вот здесь про аргументы забыли, в результате чего будет распечатан мусор.

                                              Вы неправы. Аргументы не забыли, а «мусор» будет вполне детерминированный.

                                              Перед нами хоть и грязный, но очень красивый хак, которым в те времена (да и сейчас, порой) активно пользуются в Си.

                                              Суть хака в том, что printf("%d") — вот именно так, без дополнительных параметров — выведет в stdout последнюю переменную, лежащую на стеке. Поэтому если вы, скажем, напишете:
                                              void printD(int d) {
                                                  printf("d = %d");
                                              }
                                              

                                              то получите вполне верный результат. Хотя это, разумеется, хак, но так как он основан даже не на стандарте языка, а на calling convention, то пользоваться им в некоторых случаях может быть приемлемо.

                                              P.S. В днном примере параметр будет передан, видимо, не на стеке, а в регистрах, но сути это не меняет.
                                                0
                                                В зависимости от конфигурации (debug/release) компилятор либо сгенерирует пролог (push ebp / mov ebp, esp) и тогда на стеке будет ebp, либо не сгенерирует, и на стеке будет адрес возврата, который запушен позже переданных через стек параметров. О каких переменных речь, непонятно. В любом случае, это undefined behavior и должно быть устранено.
                                                  0
                                                  Это неудачный сокращенный пример.
                                                  В реальном коде Word а там переменных навалом. Но все равно последними объявлены не те, которые надо распечатать. Так что он распечатает действительно не то:

                                                  	int iBuff;
                                                  	int cSt;            /* number of strings we will read */
                                                  	int cStRead = 0;    /* number of strings read so far */
                                                  	int cbRead;         /* number of bytes read so far */
                                                  	int cch;            /* length of st */
                                                  ...
                                                  ...
                                                  	if (cStRead == cSt)
                                                  		printf("\nCorrect number of strings were read.\n");
                                                  	else
                                                  		printf("\n - %d strings were read, %d were expected (decimal numbers) -\n");
                                                  
                                                  

                                                    0
                                                    И вообще это не код Word а, а вспомогательного инструмента:
                                                    /**  DINI.C                                              **/
                                                    /**                                                      **/
                                                    /**  This program will decode .ini files for OPUS        **/
                                                    /**  (aka Microsoft Word for Windows) and print          **/
                                                    /**  them out in human-readable format.                  **/
                                                    
                                                    
                                                    0
                                                    Мне казалось, что переменные, переданные в функцию, идут после адреса возврата, а не до. А первые несколько int-ов вообще передаются в регистрах общего назначения. Это не так?
                                                      +1
                                                      Нет, конечно. Точнее, это не всегда так, а уж в те годы — подавно. У вас регистров на 16 битной машине — раз два и закончились.

                                                      «Сначала» (старшие адреса) пушаться аргументы, справа налево для Си, потом адрес возврата, потом, если надо, формируется кадр, т.е. пушится bp, потом идут локальные переменные (младшие адреса).

                                                      FFFE | par1
                                                      FFFC | return address
                                                      FFFA | old bp
                                                      FFF8 | local var1
                                                      

                                                      В простом примере выше фрейма с bp может и не быть, т.к. нет локальных переменных.

                                                      Параметры передаются в зависимости от calling convention, для Си по умолчанию — через стек. Есть fast call или у Bolandа свой register calling convention. Есть еще pascal/fortran calling convention ну и win stdcall, конечно. Про современные 64 битные calling convention речи нет — тут регистров намного больше.
                                                        +1
                                                        После tcc (извините, что есть под руками) получаем такой код для
                                                        void printD(int d) {
                                                            printf("d = %d");
                                                        }
                                                        
                                                        void main (){
                                                        
                                                            printD(10);
                                                            return 0;
                                                        };
                                                        


                                                        00401000   55               PUSH EBP
                                                        00401001   89E5             MOV EBP,ESP
                                                        00401003   81EC 00000000    SUB ESP,0
                                                        00401009   90               NOP
                                                        0040100A   B8 00204000      MOV EAX,test5.00402000                   ; ASCII "d = %d"
                                                        0040100F   50               PUSH EAX
                                                        00401010   E8 A3000000      CALL <JMP.&msvcrt.printf>
                                                        00401015   83C4 04          ADD ESP,4
                                                        00401018   C9               LEAVE
                                                        00401019   C3               RETN
                                                        0040101A   55               PUSH EBP
                                                        0040101B   89E5             MOV EBP,ESP
                                                        0040101D   81EC 00000000    SUB ESP,0
                                                        00401023   90               NOP
                                                        00401024   B8 0A000000      MOV EAX,0A
                                                        00401029   50               PUSH EAX
                                                        0040102A   E8 D1FFFFFF      CALL test5.00401000
                                                        0040102F   83C4 04          ADD ESP,4
                                                        00401032   B8 00000000      MOV EAX,0
                                                        00401037   E9 00000000      JMP test5.0040103C
                                                        0040103C   C9               LEAVE
                                                        0040103D   C3               RETN
                                                        


                                                        0
                                                        Стек растёт в обратную сторону, поэтому мне пришлось подбирать осторожную формулировку «адрес возврата запушен позже».
                                                        В абсолютных адресах аргументы идут после адреса возврата. В любом случае, printf сначала будет забирать аргументы с меньшим адресом на стеке, т.е. сначала адрес возврата, затем параметры ф-ции.

                                                        А первые несколько int-ов вообще передаются в регистрах общего назначения

                                                        Зависит от опции компилятора, выставляющую calling convention, для __cdecl и __stdcall все параметры в стеке

                                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                  Самое читаемое