C вызов функции с слишком небольшим количеством аргументов
Я работаю над некоторым унаследованным кодом C. Первоначальный код был написан в середине 90-х годов, предназначенный для составителя Solaris и Sun C той эпохи. Текущая версия компилируется под GCC 4 (хотя и со многими предупреждениями), и, похоже, она работает, но я пытаюсь ее убрать - я хочу выжать как можно больше скрытых ошибок, поскольку я определяю, что может быть необходимо для адаптировать его к 64-битным платформам и к компиляторам, отличным от того, для которого он был создан.
Одна из моих основных задач в этом отношении заключалась в том, чтобы гарантировать, что все функции имеют прототипы (которых многие не имели), и в этом контексте я обнаружил некоторый код, который вызывает функцию (ранее не прототипированную) с меньшим количеством аргументов чем объявляется определение функции. В реализации функции используется значение отсутствующего аргумента.
Пример:
impl.c:
int foo(int one, int two) {
if (two) {
return one;
} else {
return one + 1;
}
}
client1.c:
extern foo();
int bar() {
/* only one argument(!): */
return foo(42);
}
client2.c:
extern int foo();
int (*foop)() = foo;
int baz() {
/* calls the same function as does bar(), but with two arguments: */
return (*foop)(17, 23);
}
Вопросы: это результат вызова функции с отсутствующими аргументами? Если да, какое значение получит функция для неопределенного аргумента? В противном случае компилятор Sun C ок. 1996 (для Solaris, а не VMS) демонстрируют предсказуемое поведение, специфичное для конкретной реализации, которое я могу эмулировать, добавляя определенное значение аргумента к затронутым вызовам?
Ответы
Ответ 1
EDIT: я нашел поток стека функцию C без поведения параметров, который дает очень сжатый и конкретный, точный ответ. Комментарий PMG в конце ответа taks о UB. Ниже были мои оригинальные мысли, которые, как я думаю, совпадают по строкам и объясняют, почему поведение UB..
Вопросы: это результат вызова функции с отсутствующими аргументами?
Я бы сказал, нет... Причина в том, что я думаю, что функция будет работать так: если у нее был второй параметр, но, как объясняется ниже, этот второй параметр может быть просто мусором.
Если да, какое значение получит функция для неопределенного аргумента?
Я думаю, что полученные значения undefined. Вот почему у вас может быть UB.
Есть два общих способа передачи параметров, о которых я знаю... (Wikipedia имеет хорошую страницу на соглашения о вызовах)
- Перейдите по регистру. I.e., ABI (Application Binary Interface) для формы plat будет говорить, что регистры x и y, например, предназначены для передачи в параметрах и более того, которые передаются через стек...
- Все проходит через стек...
Таким образом, когда вы даете одному модулю определение функции с "... неопределенным (но не переменным) числом параметров..." (extern def), оно не будет содержать столько параметров, сколько вы его даете ( в этом случае 1) в регистре или в месте расположения стека, в котором будет отображаться реальная функция, чтобы получить значения параметров. Поэтому вторая область для второго параметра, которая пропущена, по существу содержит случайный мусор.
EDIT: на основании другого потока стека, который я нашел, я бы поместил выше сказанное, чтобы сказать, что extern объявил функцию без параметров объявленной функции с "неопределенным (но не переменным) числом параметров".
Когда программа переходит к функции, эта функция предполагает, что механизм передачи параметров был правильно соблюден, поэтому либо просматривает регистры, либо стек, и использует любые значения, которые он находит... искушая их, чтобы они были правильными.
В противном случае компилятор Sun C ca. 1996 (для Solaris, а не VMS) продемонстрировали → предсказуемое поведение, специфичное для реализации
Вам нужно будет проверить свою документацию компилятора. Я сомневаюсь, что... определение extern будет полностью доверено, поэтому я сомневаюсь, что регистры или стек, в зависимости от механизма передачи параметров, будут правильно инициализированы...
Ответ 2
Если число или типы аргументов (после продвижения по умолчанию) не соответствуют тем, которые используются в определении фактической функции, это undefined.
Что будет на практике, зависит от реализации. Значения отсутствующих параметров не будут осмысленно определены (предполагая, что попытка получить доступ к отсутствующим аргументам не будет segfault), то есть они будут содержать непредсказуемые и, возможно, неустойчивые значения.
Будет ли программа выживать, такие неправильные вызовы также будут зависеть от соглашения о вызове. "Классическое" соглашение о вызове C, в котором вызывающий абонент несет ответственность за размещение параметров в стеке и удаление их оттуда, будет меньше подвержен ошибкам при наличии таких ошибок. То же самое можно сказать о вызовах, которые используют регистры CPU для передачи аргументов. Между тем, вызывающее соглашение, в котором сама функция несет ответственность за очистку стека, будет немедленно сработать.
Ответ 3
Очень маловероятно, что функция bar
когда-либо в прошлом давала бы согласованные результаты. Единственное, что я могу себе представить, это то, что он всегда вызывается в новом стеке и пространство стека очищается при запуске процесса, и в этом случае вторым параметром будет 0. Или разница между возвратом one
и one+1
не повлияло на большую область применения приложения.
Если это действительно так, как вы изображаете в своем примере, то вы смотрите на большую жирную ошибку. В далеком прошлом существовал стиль кодирования, в котором функции vararg
были реализованы, указав больше параметров, чем переданных, но так же, как и в случае с современными varargs
, вы не должны получать доступ к каким-либо параметрам, которые фактически не передавались.
Ответ 4
Я предполагаю, что этот код был скомпилирован и запущен в архитектуре Sun SPARC. Согласно этой старой веб-странице SPARC: "регистры %o0
- %o5
используются для первых шести параметров, переданных процедуре".
В вашем примере с функцией, ожидающей два параметра, со вторым параметром, не указанным на сайте вызова, вполне вероятно, что при выполнении вызова всегда имел место регистр %01
.
Если у вас есть доступ к исходному исполняемому файлу и вы можете разобрать код вокруг неправильного сайта вызова, вы могли бы определить, какое значение %o1
было при вызове. Или вы можете попробовать запустить исходный исполняемый файл на эмуляторе SPARC, например QEMU. В любом случае это не будет тривиальной задачей!