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. В любом случае это не будет тривиальной задачей!