Что произойдет, если я брошу указатель на функцию, изменив количество параметров

Я только начинаю обнимать указатели функций в C. Чтобы понять, как работает литье указателей функций, я написал следующую программу. Он в основном создает указатель на функцию, которая принимает один параметр, передает его указателю на функцию с тремя параметрами и вызывает функцию, предоставляя три параметра. Мне было любопытно, что произойдет:

#include <stdio.h>

int square(int val){
  return val*val;
}

void printit(void* ptr){
  int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr);
  printf("Call function with parameters 2,4,8.\n");
  printf("Result: %d\n", fptr(2,4,8));
}


int main(void)
{
    printit(square);
    return 0;
}

Это компилируется и запускается без ошибок или предупреждений (gcc -Wall на Linux/x86). Выходной сигнал в моей системе:

Call function with parameters 2,4,8.
Result: 4

Таким образом, по-видимому, лишние аргументы просто молча отбрасываются.

Теперь я хотел бы понять, что на самом деле происходит здесь.

  • Что касается законности: если я правильно понимаю ответ правильно указатель функции на другой тип, это просто поведение undefined. Так что тот факт, что это работает и дает разумный результат, - это просто удача, правильно? (или симпатией со стороны авторов компилятора).
  • Почему gcc не предупредит меня об этом, даже с Wall? Это то, что компилятор просто не может обнаружить? Почему?

Я прихожу из Java, где typechecking намного строже, поэтому это поведение немного смутило меня. Может быть, я переживаю культурный шок: -).

Ответ 1

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

Тот факт, что этот вызов работал, - это просто удача, основанная на двух фактах:

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

Компилятор не может предупредить вас о потенциальных проблемах, подобных этому по одной простой причине - в общем случае он не знает значения указателя во время компиляции, поэтому он не может оценить, на что он указывает, Представьте, что указатель функции указывает на метод в виртуальной таблице класса, который создается во время выполнения? Итак, вы сообщите компилятору, что это указатель на функцию с тремя параметрами, компилятор поверит вам.

Ответ 2

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

Ответ 3

  • Да, это поведение undefined - все может случиться, в том числе, если оно появляется "работать".

  • Листинг запрещает компилятору выдавать предупреждение. Кроме того, компиляторы не нуждаются в диагностике возможных причин поведения undefined. Причина этого заключается в том, что либо это невозможно сделать, либо сделать это было бы слишком сложно и/или вызвать много накладных расходов.

Ответ 4

Самое худшее нарушение вашего актерского поведения - это бросить указатель на указатель на функцию. Это хуже, чем изменение подписи, потому что нет гарантии, что размеры указателей функций и указателя данных равны. И вопреки множеству теоретических undefined поведений, это можно встретить в дикой природе, даже на современных машинах (не только на встроенных системах).

На встроенных платформах вы можете легко найти указатели разных размеров. Существуют даже процессоры, в которых указатели данных и указатель функций адресуют разные вещи (ОЗУ для одного, ПЗУ для другого), так называемая архитектура Гарварда. На x86 в реальном режиме вы можете смешивать 16 бит и 32 бита. У Watcom-C был специальный режим для расширителя DOS, где указатели данных были 48 бит в ширину. Особенно с C следует знать, что не все POSIX, так как C может быть единственным языком, доступным на экзотическом оборудовании.

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

Изменить: Заключение, никогда не наводите указатель на указатель на функцию.

Ответ 5

Поведение определяется соглашением о вызове. Если вы используете соглашение о вызове, в котором вызывающий пользователь выталкивает и выталкивает стек, тогда он будет работать нормально в этом случае, поскольку это просто означает, что во время вызова есть лишние байты в стеке. В настоящий момент у меня нет gcc, но с компилятором microsoft этот код:

int ( __cdecl * fptr)(int,int,int) = (int (__cdecl * ) (int,int,int)) (ptr);

Для вызова создается следующая сборка:

push        8
push        4
push        2
call        dword ptr [ebp-4]
add         esp,0Ch

Обратите внимание на 12 байтов (0Ch), добавленных в стек после вызова. После этого стеки в порядке (при условии, что вызываемый пользователь является __cdecl в этом случае, поэтому он не пытается также очистить стек). Но со следующим кодом:

int ( __stdcall * fptr)(int,int,int) = (int (__stdcall * ) (int,int,int)) (ptr);

В сборке не создается add esp,0Ch. Если вызываемый вызов __cdecl в этом случае, стек будет поврежден.

Ответ 6

  • Я не знаю точно, но вы определенно не хотите использовать это поведение, если ему повезет или если он специфичен для компилятора.

  • Это не заслуживает предупреждения, потому что приведение явно. Кастинг, вы сообщаете компилятору, что знаете лучше. В частности, вы производите void*, и поэтому вы говорите: "Возьмите адрес, представленный этим указателем, и сделайте его таким же, как этот другой указатель" - бросок просто сообщает компилятору, что вы что на целевом адресе, по сути, то же самое. Хотя здесь мы знаем, что это неверно.

Ответ 7

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

  • 1: Это не просто удача. Соглашение о вызове C четко определено, и дополнительные данные в стеке не являются фактором для сайта вызова, хотя он может быть перезаписан вызываемым пользователем, так как вызываемый не знает об этом.
  • 2: "жесткий" бросок, используя скобки, сообщает компилятору, что вы знаете, что делаете. Поскольку все необходимые данные находятся в одном блоке компиляции, компилятор может быть достаточно умным, чтобы понять, что это явно незаконно, но конструкторы C не сосредоточились на проверке ошибочной проверки угла зрения. Проще говоря, компилятор верит, что вы знаете, что делаете (возможно, неразумно в случае многих программистов на C/С++!)

Ответ 8

Чтобы ответить на ваши вопросы:

  • Чистая удача - вы можете легко растоптать стек и перезаписать указатель возврата на следующий исполняемый код. Поскольку вы указали указатель на функцию с 3 параметрами и вызвали указатель на функцию, остальные два параметра были "отброшены" и, следовательно, поведение undefined. Представьте себе, если этот 2-й или 3-й параметр содержит двоичную команду и вынул стек процедуры вызова....

  • Нет предупреждения, поскольку вы использовали указатель void * и его литье. Это вполне законный код в глазах компилятора, даже если вы явно указали переключатель -Wall. Компилятор предполагает, что вы знаете, что делаете! Это секрет.

Надеюсь, это поможет, С наилучшими пожеланиями, Том.