Комментарии 38
Спасибо!
Я редко занимаюсь низкоуровневым программированием сейчас, но в старые времена и стек приходилось раскручивать самому в некоторых ситуациях, все было канонично в стековом фрейме: аргументы в обратном порядке, EIP, EBP, локальные переменные.
Уточню — на x86 в 32-битной ОС поведения из первого примера нельзя наблюдать?
Судя по беглому взгляду на calling conventions, нет. Даже там, где что-то передается через регистры, видимо, их слишком мало, чтобы разделять floating point и integer.
Поэтому трюк работает, код
#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
Ножи очень вредные, потому что какой-нибудь ребенок может ими порезаться (они любят со всякими прикольными штуками играться).
Подобные примеры позволяют заглянуть под капот и
P.S.: Если кому интересно, type punning в другом своем проявлении (и другие пляски) затрагиваются в докладе, запись которого можно найти у меня в публикациях.
Будет куда хуже, если джуниор найдет такую возможность "методом тыка" и начнет применять, даже не зная про хрупкость подобных конструкций.
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.)
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) используется в информатике для обозначения различных техник нарушения или обмана системы типов некоторого языка программирования, имеющих эффект, который было бы затруднительно или невозможно обеспечить в рамках формального языка.
Тем не менее, стандарт не запрещает экспорт функции в одном модуле с последующим импортом в другом модуле с другим порядком аргументов.
Но это только из-за explicit каста. Если убрать каст в самом начале статьи и просто взять указатель на функцию то сообщение о несовпадении типов будет.
Если скомпилировать крайний пример c -O, то результатом other_value будет 0.000000, и это вполне логично в виду оптимизации.
Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции, который NoOp подхватывает как аргумент и возвращает.
Маленькая поправка: «Функция возвращает результат через XMM0. Между двумя функциями ничего не происходит, и в XMM0 остается результат последней функции. NoOp совсем ничего не делает (потому так и называется), так что это значение остаётся в XMM0, и принимается вызывающей стороной за результат 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++.
Я тоже не знаю зачем это сделано. Но факт остается фактом — соглашения о вызовах зависят от ОС.
Вернее сказать, конечно ОС стала предпосылкой каких-то изменений — но слово «зависит» тут явно не подходит.
Т.е. у автора, действительно, есть небрежности в формулировках, но на смысл текста они не влияют вообще никак.
Анекдот в тему:
Едут по Австралии биолог, физик и математик, и из окна видят на лугу черную овцу.
Биолог: --Смотрите, в Австралии обитают черные овцы.
Физик: --Нет, мы можем сказать лишь то, что в Австралии обитает как минимум одна черная овца.
Математик: --Нет, господа. Мы можем сказать лишь то, что в Австралии обитает как минимум одна овца, чёрная по крайней мере с одной стороны.
Автор называет «фичи наиболее распространённых компиляторов Си» просто «фичами Си», потому что для 99% программистов на Си разницы между этими формулировками нет: стандарт Си они никогда не читали, и незачем им его читать. Для них язык Си и компилятор Си — одно и то же самое.
Ну естественно, соглашение это часть ABI, если вам не нужна бинарная совместимость ни с чем, можно делать что угодно.
pc:/tmp> gcc -O2 -o a something.c -lm
pc:/tmp> ./a
(0.99)^100: 0.366032
(0.99)^100: inf
Что я делаю не так? :)
Эта "загадка" может быть поводом к тому, чтобы заинтересовать людей покопаться в том, как работает компилятор.
Но вот переход к разделу "как это применять на практике" меня просто убил.
Ещё и с примерами! (facepalm)
Каламбуры типизации функций в C