У C репутация негибкого языка. Но вы знаете, что вы можете изменить порядок аргументов функции в C, если он вам не нравится?


#include <math.h>
#include <stdio.h>

double  DoubleToTheInt(double base, int power) {
    return pow(base, power);
}

int main() {
    // приводим к указателю на функуцию с обратным порядком аргументов
    double (*IntPowerOfDouble)(int, double) =
        (double (*)(int, double))&DoubleToTheInt;

    printf("(0.99)^100: %lf \n", DoubleToTheInt(0.99, 100));
    printf("(0.99)^100: %lf \n", IntPowerOfDouble(100, 0.99));
}

Этот код на самом деле никогда не определяет функцию IntPowerOfDouble — потому что функции IntPowerOfDouble не существует. Это переменная, указывающая на DoubleToTheInt, но с типом, который говорит, что ему хочется, чтобы аргумент типа int шел перед аргументом типа double.


Вы могли бы ожидать, что IntPowerOfDouble примет аргументы в том же порядке, что и DoubleToTheInt, но приведет аргументы к другим типам, или что-то типа того. Но это не то, что происходит.


Попробуйте — вы увидите одинаковый результат в обоих строчках.


emiller@gibbon ~> clang something.c 
emiller@gibbon ~> ./a.out 
(0.99)^100: 0.366032 
(0.99)^100: 0.366032 

Теперь попробуйте изменить все int на float — вы увидите, что FloatPowerOfDouble делает что-то ещё более странное. Да,


double  DoubleToTheFloat(double base, float power) {
    return pow(base, power);
}

int main() {
    double (*FloatPowerOfDouble)(float, double) =
        (double (*)(float, double))&DoubleToTheFloat;

    printf("(0.99)^100: %lf \n", DoubleToTheFloat(0.99, 100));   // OK
    printf("(0.99)^100: %lf \n", FloatPowerOfDouble(100, 0.99)); // Упс...
}

выдает:


(0.99)^100: 0.366032 
(0.99)^100: 0.000000 

Значение во второй строке "даже не ошибочное" — если бы проблема была в перестановке аргументов, мы бы ожидали, что ответ будет 100^0.99 = 95.5 а не 0. Что происходит?


Примеры кода выше представляют каламбуры типизации функций(type punning of functions) — опасную форму "ассемблера без ассемблера" который никогда не должен использоваться на работе, рядом с тяжелой техникой или в сочетании с отпускаемыми по рецепту лекарствами. Эти примеры абсолютно логичны для тех, кто понимает код на уровне ассемблера — но, скорее всего, запутает всех остальных.


Я немного смухлевал — предположил, что вы запустите код на 64-битном x86 компьютере. На другой архитектуре этот фокус может не сработать. Хоть и считается, что у C бесконечное количество темных углов, поведение с аргументами int и double точно не является частью стандарта C. Это результат того, как вызываются функции на современных x86 машинах, и может быть использовано для изящных программистских трюков.


Это не моя сигнатура


Если вы изучали C в университете, вы может быть помните, что аргументы передаются функции на стек. Вызывающий кладет аргументы на стек в обратном порядке, а функция считывает аргументы со стека.


По крайней мере, мне объяснили это именно так, но большинство компьютеров сегодня передают первые несколько аргументов прямо в регистры CPU. Таким образом функции не понадобится читать из стека, что гораздо медленнее регистров.


Количество и расположение регистров, используемых для аргументов функций зависит от соглашения о вызовах(calling convention). У Windows одно соглашение — четыре регистра для значений с плавающей точкой и четыре регистра для указателей и целых чисел. у Unix другое соглашение, называющееся соглашение System V. В нём для аргументов с плавающей точкой предназначено восемь регистров и еще шесть — для указателей и целых чисел. (Если аргументы не влазят в регистры, то они отправляются по старому на стек.)


В C, заголовочные файлы существуют только чтобы сказать компилятору, куда класть аргументы функции, зачастую комбинируя регистры и стек. У каждого соглашения о вызовах есть свой алгоритм для расположения этих аргументов в регистрах и на стеке. Unix, например, очень агрессивен насчет разбивания структур и попыток уместить все поля в регистрах, в то время как Windows немного ленивее и просто передает указатель на большую структуру-параметр.


Но и в Windows, и в Unix, базовый алгоритм работает так:


  • Аргументы с плавающей точкой расположены, по порядку, в регистрах SSE, обозначенных XMM0, XMM1 и т.д.
  • Целые и указатели расположены, по порядку, в регистрах общего назначения, обозначенных RDX, RCX и т.д.

Давайте посмотрим, как передаются аргументы функции DoubleToTheInt.


Сигнатура функции такова:


  double  DoubleToTheInt(double base, int power);

Когда компилятор встречает DoubleToTheInt(0.99, 100), он располагает регистры так:


RDX RCX R8 R9
100 ??? ??? ???
XMM0 XMM1 XMM2 XMM3
0.99 ??? ??? ???

(Для простоты, я использую соглашение о вызовах Windows.) Если бы взамен была такая функция:


  double  DoubleToTheDouble(double base, double power);

Аргументы были бы расположены так:


RDX RCX R8 R9
??? ??? ??? ???
XMM0 XMM1 XMM2 XMM3
0.99 100 ??? ???

Теперь вы, возможно, догадались, почему маленьких фокус из начала статьи работает. Рассмотрим следующую сигнатуру функции:


  double IntPowerOfDouble(int y, double x);

Вызывая IntPowerOfDouble(100, 0.99), компилятор расположит регистры так:


RDX RCX R8 R9
100 ??? ??? ???
XMM0 XMM1 XMM2 XMM3
0.99 ??? ??? ???

Другими словами, точно так же, как в DoubleToTheInt(0.99, 100)!
Из-за того, что скомпилированная функция понятия не имеет, как она была вызвана — только где в регистрах и на стеке ожидать свои аргументы — мы можем вызвать функцию с другим порядком аргументов приведя указатель на функцию к неверной (но ABI-совместимой) сигнатуре функции.


Фактически, пока целые аргументы и аргументы с плавающей точкой сохраняют порядок, мы можем перемешивать их как угодно, и расположение регистров будет одинаковым. То есть, у


double functionA(double a, double b, float c, int x, int y, int z);


будет такое же расположение регистров, как и у:


double functionB(int x, double a, int y, double b, int z, float c);


и такое же, как у:


double functionC(int x, int y, int z, double a, double b, float c);


Во всех трех случаях в регистрах будет:


RDX RCX R8 R9
int x int y int z ???
XMM0 XMM1 XMM2 XMM3
double a double b double c ???

Обратите внимание, что и аргументы двойной, и аргументы одинарной точности занимают регистры XMM — но они не ABI-совместимы друг с другом. Поэтому, если вы помните второй пример кода в самом начале, причина по которой FloatPowerOfDouble вернул ноль (а не 95.5) в том, что компилятор расположил значение одинарной точности (32-битное) 100.0 в XMM0, и значение двойной точности (64-битное) 0.99 в XMM1 — но вызываемая функция ожидала число двойной-точности в XMM0 и одинарной в XMM1. Из-за этого, экспонента притворилась мантиссой, биты мантиссы были обрезаны или приняты за экспоненту, и функция FloatPowerOfDouble возвела Очень Маленькое Число в степень Очень Большого Числа, получив ноль. Загадка решена.


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


Это создает интересную возможность — вдобавок к вызову функции с другим порядком аргументов, также можно вызвать функцию с другим количеством аргументов. Есть несколько причин, по которым можно захотеть сделать что-то настолько сумасшедшее.


Наберите 1-800-I-Really-Enjoy-Type-Punning


Попробуйте это:


#include <math.h>
#include <stdio.h>

double  DoubleToTheInt(double x, int y) {
    return pow(x, y);
}

int main() {
    double (*DoubleToTheIntVerbose)(
            double, double, double, double, int, int, int, int) =
    (double (*)(double, double, double, double, int, int, int, int))&DoubleToTheInt;

      printf("(0.99)^100: %lf \n", DoubleToTheIntVerbose(
                                   0.99, 0.0, 0.0, 0.0, 100, 0, 0, 0));
      printf("(0.99)^100: %lf \n", DoubleToTheInt(0.99, 100));
}

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


Теперь начинается веселье. Мы можем определить новый "многословный" тип функции который может вызывать много разных типов функций, при условии что аргументы влазят в регистры и функцию возвращают один и тот же тип.


#include <math.h>
#include <stdio.h>

typedef double (*verbose_func_t)(double, double, double, double, int, int, int, int);

int main() {
    verbose_func_t verboseSin = (verbose_func_t)&sin;
    verbose_func_t verboseCos = (verbose_func_t)&cos;
    verbose_func_t verbosePow = (verbose_func_t)&pow;
    verbose_func_t verboseLDExp = (verbose_func_t)&ldexp;

    printf("Sin(0.5) = %lf\n",
        verboseSin(0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0));
    printf("Cos(0.5) = %lf\n",
        verboseCos(0.5, 0.0, 0.0, 0.0, 0, 0, 0, 0));
    printf("Pow(0.99, 100) = %lf\n",
        verbosePow(0.99, 100.0, 0.0, 0.0, 0, 0, 0, 0));
    printf("0.99 * 2^12 = %lf\n",
        verboseLDExp(0.99, 0.0, 0.0, 0.0, 12, 0, 0, 0));
}

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


#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef double (*four_arg_func_t)(double, double, double, double);

int main(int argc, char **argv) {
    four_arg_func_t verboseFunction = NULL;
    if (strcmp(argv[1], "sin") == 0) {
        verboseFunction = (four_arg_func_t)&sin;
    } else if (strcmp(argv[1], "cos") == 0) {
        verboseFunction = (four_arg_func_t)&cos;
    } else if (strcmp(argv[1], "pow") == 0) {
        verboseFunction = (four_arg_func_t)&pow;
    } else {
        return 1;
    }
    double xmm[4];
    int i;
    for (i=2; i<argc; i++) {
        xmm[i-2] = strtod(argv[i], NULL);
    }

    printf("%lf\n", verboseFunction(xmm[0], xmm[1], xmm[2], xmm[3]));
    return 0;
}

Проверяем:


emiller@gibbon ~> clang calc.c
emiller@gibbon ~> ./a.out pow 0.99 100
0.366032
emiller@gibbon ~> ./a.out sin 0.5
0.479426
emiller@gibbon ~> ./a.out cos 0.5
0.877583

Не совсем конкурент Mathematica, но можно представить более сложную версию с таблицей имен функций и соответсвующих им указателей на функцию — для добавления новой функции достаточно обновить таблицу, а не явно вызывать новую функцию в коде.


Другое применение включает JIT компиляторы. Если вы когда-нибудь занимались по туториалу LLVM, вы могли неожиданно встретить сообщение:


"Full-featured argument passing not supported yet!"

LLVM искусно превращает код в машинные коды и загружает машинные коды в память, но не очень гибок, если нужно вызвать загруженную в память функцию. С помощью LLVMRunFunction, вы можете вызывать main()-подобные функции (целый аргумент, аргумент-указатель, аргумент-указатель, возвращает целое), но не многое другое. Большинство туториалов рекомендует обернуть вашу функцию компилятора функцией похожей на main(), пряча все ваши аргументы за аргументом-указателем, и использовать обертку чтобы вытянуть аргументы из указателя и вызвать настоящую функцию.


Но с нашими новыми знаниями о регистрах X86, мы можем упростить церемонию, избавившись от функции-обертки во многих случаях. Вместо того, чтобы проверять, что функция пренадлежит к ограниченному списку C-callable сигнатур функций (int main(), int main(int), int main(int, void *) и т.д.), мы можем создать указатель, сигнатура которго заполняет все регистры параметров и, следовантельно, совместима со всеми функциями, которые передают аргументы только через регистры, и вызывать их, передавая ноль (или что угодно) для неиспользуемых аргументов. Нам надо всего лишь определить отдельный тип для каждого возвращаемого типа, а не для каждой возможной сигнатуры функции, и более гибко вызывать функции с помощью способа, который в другом случае потребовал бы использование ассемблера.


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


double NoOp(double a) {
    return a;
}

int main() {
    double (*ReturnLastReturnValue)() = (double (*)())&NoOp;
    double value = pow(0.99, 100.0);
    double other_value = ReturnLastReturnValue();
    printf("Value: %lf   Other value: %lf\n" value, other_value);
}

(Вам стоит для начала прочитать ваше соглашение о вызовах...)


Теория переводчика

Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции, который NoOp подхватывает как аргумент и возвращает.


Требуется немного ассемблера


Если вы когда-нибудь спросите на форуме программистов об ассемблере, обычным ответом будет: Тебе не нужен ассемблер — оставь его для гениальных докторов наук, которые пишут компиляторы. Да, держи пожалуйста руки на виду.


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


Но это лишь самая вершина айсберга программирования на ассемблере — специально представленная без единой строчки кода на ассемблере — и я советую всем, у кого есть время, поглубже окунуться в эту тему. Ассемблер — ключ к пониманию, как CPU занимается исполнением инструкций — что такое счетчик команд, что такое указатель фрейма, что такое указатель стека, что делают регистры — и позволяет вам посмотреть на программы в другом (более ярком) свете. Даже базовые знания могут помочь вам придумать решения, которые в ином случае даже не пришли бы вам в голову и понять что к чему, когда вы проскользнете мимо тюремных надзирателей своего любимого языка высокого уровня и будете щуриться на суровое, прекрасное солнце.

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

Проголосовало 395 человек. Воздержалось 55 человек.

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

Поделиться с друзьями
-->

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


  1. iCpu
    14.08.2016 23:12
    +12

    Изящный способ отстрелить себе голову ногу по самую голову.

    Спасибо!


  1. QtRoS
    14.08.2016 23:12

    Уточню — на x86 в 32-битной ОС поведения из первого примера нельзя наблюдать?
    Я редко занимаюсь низкоуровневым программированием сейчас, но в старые времена и стек приходилось раскручивать самому в некоторых ситуациях, все было канонично в стековом фрейме: аргументы в обратном порядке, EIP, EBP, локальные переменные.


    1. qw1
      14.08.2016 23:25
      +5

      На x86 было несколько ABI: __cdecl, __stdcall и __fastcall. Последний передавал два аргумента в регистрах, а остальные через стек. Так что зависело от опций компилятора, многие проекты в Release ставили __fastcall, чтобы оптимизировать код.


    1. Randl
      14.08.2016 23:44

      Уточню — на x86 в 32-битной ОС поведения из первого примера нельзя наблюдать?

      Судя по беглому взгляду на calling conventions, нет. Даже там, где что-то передается через регистры, видимо, их слишком мало, чтобы разделять floating point и integer.


      1. qw1
        15.08.2016 00:38
        +7

        В __fastcall int-аргументы передаются черех ecx, edx, а floating-point — всегда через стек.
        Поэтому трюк работает, код

        #include <stdio.h>
        
        void __fastcall test(double x, int a, int b)
        {
                printf("%f %d %d\n", x, a, b);
        }
        
        int main()
        {
                void (__fastcall *runme)(int, int, double) = (void (__fastcall *)(int, int, double))&test;
                runme(3, 5, 3.14);
        }
        выводит
        3.140000 3 5


        1. Randl
          15.08.2016 00:48

          Спасибо за уточнение


  1. Deosis
    15.08.2016 06:22
    +3

    Красиво, но не переносимо, Сима!


  1. tvi
    15.08.2016 07:54
    -6

    Это платформозависимые пляски. Почему нельзя объявить inline функцию, в которой «поправить» расположение: и код будет переносим, а главное понятен и компилятору и программеру?


  1. hdfan2
    15.08.2016 07:57
    -2

    Очень вредная статья, я так считаю. Какой-нибудь джуниор запилит такое (они любят всякие прикольные штуки использовать), а потом кто-нибудь (скорее всего, вообще не он) поимеет несколько восхитительных часов отладки. Такие штуки, как и оружие, нужно выдавать только после длительных тренировок и после медкомиссии. И сажать за нелегальное использование.


    1. Randl
      15.08.2016 08:02
      +7

      Ножи очень вредные, потому что какой-нибудь ребенок может ими порезаться (они любят со всякими прикольными штуками играться).


    1. Halt
      15.08.2016 08:10
      +5

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

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

      P.S.: Если кому интересно, type punning в другом своем проявлении (и другие пляски) затрагиваются в докладе, запись которого можно найти у меня в публикациях.


    1. mayorovp
      15.08.2016 09:37
      +3

      Будет куда хуже, если джуниор найдет такую возможность "методом тыка" и начнет применять, даже не зная про хрупкость подобных конструкций.


    1. gaki
      16.08.2016 08:11

      В статье несколько раз прямым текстом говорится, что так делать _не надо_. Если этот джуниор настолько упорот, что всё равно не понимает, то он не на этом, так на чём другом рано или поздно отстрелит себе голову и всех вокруг забрызгает.


  1. HighPredator
    15.08.2016 09:44
    +9

    Это не каламбур. Это неопределенное поведение. Дело в том, что осуществив каст к указателю на функцию с обратным порядком аргументов, вы получаете UB согласно стандарта языка (

    C11, раздел 6.3.2.3, параграф 8
    A pointer to a function of one type may be converted to a pointer to a function of another
    type and back again; the result shall compare equal to the original pointer. If a converted
    pointer is used to call a function whose type is not compatible with the referenced type,
    the behavior is undefined.


    1. Randl
      15.08.2016 09:47
      +4

      Термин каламбур типизации (англ. type punning) используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования, имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка.


      1. HighPredator
        15.08.2016 09:52
        +1

        Вот и я что-то узнал. Спасибо.


    1. mayorovp
      15.08.2016 09:49

      Тем не менее, стандарт не запрещает экспорт функции в одном модуле с последующим импортом в другом модуле с другим порядком аргументов.


    1. GarryC
      15.08.2016 10:31
      +1

      А мы не должны получить предупреждение? Если пишем поперек стандарта? Или С думает, что мы понимаем, что делаем, раз есть явное приведение?


      1. tyomitch
        15.08.2016 11:35

        «Должны ли?» — в смысле, «требует ли этого стандарт?» (нет, не требует) или «не лучше ли было бы, если бы выдавалось предупреждение?» (да, было бы лучше. Если вы предложите соответствующий патч для clang, его бы наверняка приняли.)


      1. wrfsh
        15.08.2016 11:41

        В стандарте описаны ситуации когда «реализация языка» должна генерировать «диагностические сообщения», а когда нет. По стандарту при использовании explicit casts предупреждение не обязательно.

        Но это только из-за explicit каста. Если убрать каст в самом начале статьи и просто взять указатель на функцию то сообщение о несовпадении типов будет.


  1. iyemelyanov
    15.08.2016 11:36
    +1

    Спасибо за статью! )

    Если скомпилировать крайний пример c -O, то результатом other_value будет 0.000000, и это вполне логично в виду оптимизации.


  1. tyomitch
    15.08.2016 11:39
    +1

    Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции, который NoOp подхватывает как аргумент и возвращает.

    Маленькая поправка: «Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции. NoOp совсем ничего не делает (потому так и называется), так что это значение остаётся в XMM0, и принимается вызывающей стороной за результат NoOp.»


    1. tyomitch
      15.08.2016 12:17

      Добавлю ещё, что объявление

      void NoOp() {}
      полностью равнозначно приведённому в том примере.


  1. fsmoke
    15.08.2016 11:42
    +6

    Статья интересная, хоть и немного неграмотная, например фразы:

    «большинство компьютеров сегодня передают первые несколько аргументов прямо в регистры»
    «У Windows одно соглашение....Unix другое соглашение»
    «Unix, например, очень агрессивен насчет разбивания структур...Windows немного ленивее»

    это явная глупость, у начинающих разработчиков может сложиться впечатление, что передача аргументов может зависеть от ОС или ещё абсурднее от компьютера(я имею ввиду в рамках CISC архитектуры)

    Если смотреть с точки зрения низкого уровня: Call инструкция — (грубо) это всего лишь переход с возможностью возврата. Как я буду передавать аргументы — это сугубо моё личное дело, я могу написать свой компилятор С, который будет поддерживать какую нибудь извратную конвенцию вызова и это будет работать и под виндой и под никсами! И никто мне этого не запретит — ни ОС ни тем более процессор, который об этом вообще ничего не знает. Другое дело, что если мне потребуется сделать системный вызов к ОС — я вынужден буду использовать конвенцию навязанную мне разработчиками ОС в рамках конкретного ABI — но это совсем уже другая история.

    Т.е. и под никсами и под виндой я могу написать и собрать исполняемый бинарник и какие нибудь библиотеки(dll/so) — между которыми будут происходить вызовы как угодно, хоть через стек, хоть через регистры, через память, вперед, назад, из центра(шутка) — как мне захочется. Тем более если у меня монолитный исполняемый файл(как примеры из статьи) — я вообще могу делать, что захочу с моим кодом. Плюс существуют оптимизации, которые при любом удобном случае скорее всего будут пытаться передать аргументы наиболее оптимально..., но это тоже относится лишь к компилятору.

    И ограничен я лишь возможностями компилятора, естественно на свете уже есть ряд общепринятых CC, ещё раз повторюсь — если я в компиляторе поддержу какую нибудь свою конвенцию, никакие ОС меня не остановят и вообще по большому счету не будут знать, что там у меня происходит.

    PS
    Предлагаю переводчику, сделать ремарки, о том что все вызовы зависят от компилятора, а не от ОС и тем более «компьютеров». Я прекрасно могу иcпользовать ABI никсовых компиляторов в Windows, если это не затрагивает системные вызовы. Но даже если брать вызовы WinApi — они происходят в рамках stdcall конвенции, в отличии от всех остальных вызовов внутри приложения, короче говоря в статье идет смесь понятий CC и ABI, одна и та же СС может в разных ABI выглядеть по разному. И это всё относится лишь к компиляторам…


    1. mayorovp
      15.08.2016 12:11

      Но соглашения о вызовах и правда зависят от ОС. Например, соглашение thiscall отличается в linux g++ и mingw g++.


      1. fsmoke
        15.08.2016 12:21

        не знаю зачем это было сделано, возможно thiscall отличается специально, для поддержки каких нибудь COM интерфейсов Windows или ещё чего нибудь, чтобы было проще, хотя врать не буду — это просто предположение


        1. mayorovp
          15.08.2016 12:27

          Я тоже не знаю зачем это сделано. Но факт остается фактом — соглашения о вызовах зависят от ОС.


          1. fsmoke
            15.08.2016 12:36

            Да не от ОС это зависит… Вы хотите сказать, что если я сейчас возьму исходники mingw, поправлю их, чтобы thiscall соответствовал никсовому gcc — у меня перестанет работать какое нибудь консольное приложение, которое особо ничего не дергает? Не будет такого.

            Вернее сказать, конечно ОС стала предпосылкой каких-то изменений — но слово «зависит» тут явно не подходит.


            1. mayorovp
              15.08.2016 12:43

              Работать-то ничего не перестанет — но собранное новым компилятором консольное приложение перестанет быть бинарно совместимым со старым.


    1. tyomitch
      15.08.2016 12:12
      +1

      Вы мыслите, как computer scientist (сферический компилятор в вакууме может передавать параметры как угодно), а автор статьи — как software engineer (компиляторы, практически применяемые в софтописании, передают параметры вполне определённым образом).
      Т.е. у автора, действительно, есть небрежности в формулировках, но на смысл текста они не влияют вообще никак.

      Анекдот в тему:

      Едут по Австралии биолог, физик и математик, и из окна видят на лугу черную овцу.
      Биолог: --Смотрите, в Австралии обитают черные овцы.
      Физик: --Нет, мы можем сказать лишь то, что в Австралии обитает как минимум одна черная овца.
      Математик: --Нет, господа. Мы можем сказать лишь то, что в Австралии обитает как минимум одна овца, чёрная по крайней мере с одной стороны.


      1. fsmoke
        15.08.2016 12:28

        В целом согласен, но фраза «вы можете изменить порядок аргументов функции в C» имхо слишком небрежна. Автор порой выдает UB за какие-то «фичи». Хотя эти самые фичи — и являются сферическим чем-то там, т.к. работают только в определенном компиляторе, определенной версии, под определенную разрядность определенной архитектуры.


        1. tyomitch
          15.08.2016 13:02
          +1

          То, что с точки зрения стандарта является UB, очень часто с точки зрения компилятора является фичей этого компилятора.

          Автор называет «фичи наиболее распространённых компиляторов Си» просто «фичами Си», потому что для 99% программистов на Си разницы между этими формулировками нет: стандарт Си они никогда не читали, и незачем им его читать. Для них язык Си и компилятор Си — одно и то же самое.


    1. Randl
      15.08.2016 13:04

      Ну естественно, соглашение это часть ABI, если вам не нужна бинарная совместимость ни с чем, можно делать что угодно.


  1. dkv
    15.08.2016 14:19
    +3

    Век живи, век учись эффективно стрелять себе в ногу с рикошетом в голову. Спасибо за статью.


  1. ChachaB
    15.08.2016 19:16
    +2

    Хм, при чем тут С? Похоже автор путает термины компилятор и язык.

    pc:/tmp> gcc -O2 -o a something.c -lm
    pc:/tmp> ./a
    (0.99)^100: 0.366032 
    (0.99)^100: inf
    

    Что я делаю не так? :)


    1. tyomitch
      15.08.2016 20:42

      И действительно, какое отношение компилятор Си имеет к Си? :)


    1. Randl
      16.08.2016 05:02

      Без листинга ассемблера это не определить. Впрочем, лично я (как и большинство здесь) даже с листингом не уверен что разберусь.


  1. Sliver
    17.08.2016 15:58
    +1

    Эта "загадка" может быть поводом к тому, чтобы заинтересовать людей покопаться в том, как работает компилятор.


    Но вот переход к разделу "как это применять на практике" меня просто убил.
    Ещё и с примерами! (facepalm)