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

StackOverflow https://stackoverflow.com/questions/2118889

Вопрос

Я только начинаю обернуть голову по указателям функций в 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 не предупреждает меня об этом, даже со стеной? Это то, что компилятор просто не может обнаружить? Почему?

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

Это было полезно?

Решение

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

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

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

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

Другие советы

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

  1. Да, это неопределенное поведение - может произойти все, в том числе оно, казалось бы, «работа».

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

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

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

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

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

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

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

А add esp,0Ch не генерируется в сборке. Если в этом случае Callee __CDECL, стек будет поврежден.

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

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

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

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

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

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

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

Надеюсь, это поможет, с уважением, Том.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top