Что произойдет, если я разыграю указатель функции, изменяя количество параметров
-
22-09-2019 - |
Вопрос
Я только начинаю обернуть голову по указателям функций в 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
Так что, очевидно, лишние аргументы просто молча отброшены.
Теперь я хотел бы понять, что на самом деле здесь происходит.
- Что касается законности: если я понимаю ответ на Согласно указателю функции на другой тип Правильно, это просто неопределенное поведение. Таким образом, тот факт, что это работает и дает разумный результат, является просто удачей, правильно? (или приятная со стороны писателей компиляторов)
- Почему GCC не предупреждает меня об этом, даже со стеной? Это то, что компилятор просто не может обнаружить? Почему?
Я приезжаю из Java, где Typechecking намного строго, поэтому это поведение немного смутило меня. Может быть, я испытываю культурный шок :-).
Решение
Дополнительные параметры не отброшены. Они должным образом размещены в стеке, как будто вызов выполняется для функции, которая ожидает три параметра. Однако, поскольку ваша функция заботится только об одном параметре, она выглядит только в верхней части стека и не касается других параметров.
Тот факт, что этот вызов сработал, является чистой удачей, основываясь на двух фактах:
- Тип первого параметра одинаково для функции и указателя листа. Если вы измените функцию, чтобы взять указатель на строку и попытаться распечатать эту строку, вы получите хороший сбой, так как код попытается определить указатель для решения памяти 2.
- Призывное соглашение, используемое по умолчанию, предназначено для абонента для очистки стека. Если вы измените вызову, так что Callee очистит стек, вы получите абонент, выдвигающий три параметра в стеке, а затем очистка Callee (или, скорее, пытается) один параметр. Это, вероятно, приведет к коррупции стека.
Компилятор не может предупредить вас о потенциальных проблемах, подобных этой, по одной простой причине - в общем случае он не знает значения указателя во время компиляции, поэтому он не может оценить, на что он указывает. Представьте, что указатель функции указывает на метод в виртуальной таблице класса, который создается во время выполнения? Итак, вы говорите компилятору, это указатель на функцию с тремя параметрами, компилятор поверит вам.
Другие советы
Если вы возьмете машину и бросаете ее в молоток, компилятор возьмет вас за слово, что автомобиль - молоток, но это не превращает машину в молоток. Компилятор может быть успешным в использовании автомобиля для вождения гвоздь, но это зависит от реализации. Это все еще неразумно.
Да, это неопределенное поведение - может произойти все, в том числе оно, казалось бы, «работа».
Актерский состав не позволяет компилятору выдавать предупреждение. Кроме того, компиляторы не требуют диагностики возможных причин неопределенного поведения. Причина этого в том, что это либо невозможно сделать, либо это было бы слишком сложно и/или привести к большому количеству накладных расходов.
Худшее правонарушение в вашем актере - разыграть указатель данных на указатель функции. Это хуже, чем изменение подписи, потому что нет никакой гарантии, что размеры указателей функций и указателя данных равны. И вопреки многим теоретический Неопределенное поведение, это может встречаться в дикой природе, даже на передовых машинах (не только на встроенных системах).
Вы можете легко встретить различные указатели размера на встроенных платформах. Существуют даже процессоры, где указатели данных и указатель функций занимаются разными вещами (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, стек будет поврежден.
Признано я не знаю наверняка, но вы определенно не хотите воспользоваться поведением, если это удача или же Если это специфично для компилятора.
Это не заслуживает предупреждения, потому что актерский состав явный. С катированием вы сообщаете компилятору, что вы знаете лучше. В частности, вы играете
void*
, и как таковое, вы говорите: «Возьмите адрес, представленный этим указателем, и сделайте его таким же, как и этот другой указатель» - актерский состав просто сообщает компилятору, что вы уверены, что на целевом адресе, на самом деле, одинаковый. Хотя здесь мы знаем, что это неправильно.
В какой -то момент я должен освежить свою память о бинарной планировке Собработки C, но я уверен, что это то, что происходит:
- 1: Это не чистая удача. Конвенция о вызове C является четко определенной, и дополнительные данные в стеке не являются фактором для сайта вызова, хотя он может быть перезаписан Callee, поскольку Callee не знает об этом.
- 2: «жесткий» актерский состав, использующий скобку, говорит компилятору, что вы знаете, что делаете. Поскольку все необходимые данные находятся в одном компиляционном блоке, компилятор может быть достаточно умным, чтобы выяснить, что это явно незаконно, но дизайнер (ы) C не был сосредоточен на том, чтобы поймать угловой чехол, проверенный неверно. Проще говоря, компилятор доверяет, что вы знаете, что вы делаете (возможно, неразумно в случае многих программистов C/C ++!)
Чтобы ответить на ваши вопросы:
Чистая удача - вы можете легко растопить стек и перезаписать указатель возврата к следующему коду выполнения. Поскольку вы указали указатель функции с 3 параметрами и вызывая указатель функции, оставшиеся два параметра были «отброшены», и, следовательно, поведение не определен. Представьте себе, если этот 2 -й или 3 -й параметр содержал двоичную инструкцию и отключил стек процедур вызова ....
Нет предупреждения, так как вы использовали
void *
указатель и отбрасывать его. Это довольно законный код в глазах компилятора, даже если вы явно указали-Wall
выключатель. Компилятор предполагает, что вы знаете, что делаете! Это секрет.
Надеюсь, это поможет, с уважением, Том.