Как стать автором
Обновить

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

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

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

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

В __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

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

Красиво, но не переносимо, Сима!
Это платформозависимые пляски. Почему нельзя объявить inline функцию, в которой «поправить» расположение: и код будет переносим, а главное понятен и компилятору и программеру?
Очень вредная статья, я так считаю. Какой-нибудь джуниор запилит такое (они любят всякие прикольные штуки использовать), а потом кто-нибудь (скорее всего, вообще не он) поимеет несколько восхитительных часов отладки. Такие штуки, как и оружие, нужно выдавать только после длительных тренировок и после медкомиссии. И сажать за нелегальное использование.

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

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

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

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

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

В статье несколько раз прямым текстом говорится, что так делать _не надо_. Если этот джуниор настолько упорот, что всё равно не понимает, то он не на этом, так на чём другом рано или поздно отстрелит себе голову и всех вокруг забрызгает.
Это не каламбур. Это неопределенное поведение. Дело в том, что осуществив каст к указателю на функцию с обратным порядком аргументов, вы получаете 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.

)
из-за того, что указатели не совместимы. И вот почему. Указатели на функции считаются совместимыми тогда и только тогда, когда: (C11, раздел 6.7.6.3, параграф 15:
For two function types to be compatible, both shall specify compatible return types.146)
Moreover, the parameter type lists, if both are present, shall agree in the number of
parameters and in use of the ellipsis terminator; corresponding parameters shall have
compatible types
. If one type has a parameter type list and the other type is specified by a
function declarator that is not part of a function definition and that contains an empty
identifier list, the parameter list shall not have an ellipsis terminator and the type of each
parameter shall be compatible with the type that results from the application of the
default argument promotions. If one type has a parameter type list and the other type is
specified by a function definition that contains a (possibly empty) identifier list, both shall
agree in the number of parameters, and the type of each prototype parameter shall be
compatible with the type that results from the application of the default argument
promotions to the type of the corresponding identifier. (In the determination of type
compatibility and of a composite type, each parameter declared with function or array
type is taken as having the adjusted type and each parameter declared with qualified type
is taken as having the unqualified version of its declared type.)
) Из этого нас в данном случае интересует только выделенное жирным, поскольку остальное (в частности тип возвращаемого результата) нас не интересует. А выделенное четко говорит о том, что типы соответствующих (т.е. в отношении порядка следования) параметров должны быть совместимы. Для простых случаев вроде этого (т.е. без использования доп. правил из стандарта), типы считаются совместимыми или «имеющими совместный тип» тогда, когда они одинаковы (
C11, раздел 6.2.7, параграф 1
Tw o types have compatible type if their types are the same. Additional rules for
determining whether two types are compatible are described in 6.7.2 for type specifiers,
in 6.7.3 for type qualifiers, and in 6.7.6 for declarators.55) Moreover, two structure,
union, or enumerated types declared in separate translation units are compatible if their
tags and members satisfy the following requirements: If one is declared with a tag, the
other shall be declared with the same tag. If both are completed anywhere within their
respective translation units, then the following additional requirements apply: there shall
be a one-to-one correspondence between their members such that each pair of
corresponding members are declared with compatible types; if one member of the pair is
declared with an alignment specifier, the other is declared with an equivalent alignment
specifier; and if one member of the pair is declared with a name, the other is declared
with the same name. For two structures, corresponding members shall be declared in the
same order. For two structures or unions, corresponding bit-fields shall have the same
widths. For two enumerations, corresponding members shall have the same values.

Все.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Век живи, век учись эффективно стрелять себе в ногу с рикошетом в голову. Спасибо за статью.
Хм, при чем тут С? Похоже автор путает термины компилятор и язык.
pc:/tmp> gcc -O2 -o a something.c -lm
pc:/tmp> ./a
(0.99)^100: 0.366032 
(0.99)^100: inf

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

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

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


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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации