Что произойдет, если я приведу указатель на функцию, изменив количество параметров - PullRequest
10 голосов
/ 22 января 2010

Я только начинаю оборачиваться вокруг указателей на функции в 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

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

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

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

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

Ответы [ 8 ]

15 голосов
/ 22 января 2010

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

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

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

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

12 голосов
/ 22 января 2010

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

3 голосов
/ 22 января 2010

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

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

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

Редактировать: Заключение, никогда не приводить указатель данных к указателю функции.

3 голосов
/ 22 января 2010
  1. Да, это неопределенное поведение - может произойти все, в том числе и «работать».

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

2 голосов
/ 22 января 2010

Поведение определяется соглашением о вызовах. Если вы используете соглашение о вызовах, когда вызывающий объект выталкивает и извлекает стек, то в этом случае он будет работать нормально, так как это будет означать, что во время вызова в стеке будет несколько дополнительных байтов. У меня нет 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, стек будет поврежден.

1 голос
/ 22 января 2010

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

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

  2. Это не заслуживает предупреждения, потому что приведение является явным. При кастинге вы сообщаете компилятору, что знаете лучше. В частности, вы приводите void*, и поэтому вы говорите «возьмите адрес, представленный этим указателем, и сделайте его таким же, как этот другой указатель» - приведение просто сообщает компилятору, что вы уверен, что то, что находится по адресу назначения, на самом деле то же самое. Хотя здесь мы знаем, что это неправильно.

0 голосов
/ 22 января 2010

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

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

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

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

...