Деякі особливості мови C починалися як хаки, які щойно спрацювали.
Кілька підписів для основних, а також списків аргументів змінної довжини, є однією з цих особливостей.
Програмісти помітили, що вони можуть передавати додаткові аргументи функції, і нічого поганого не відбувається з даним компілятором.
Це має місце, якщо умови виклику такі, що:
- Виклична функція очищає аргументи.
- Крайні ліві аргументи знаходяться ближче до верхньої частини стека або до основи кадру стека, так що помилкові аргументи не роблять адресацію недійсною.
Одним із наборів домовленостей виклику, який підпорядковується цим правилам, є передача параметрів на основі стеку, за допомогою якої виклик вискакує аргументи, і вони рухаються справа наліво:
;; pseudo-assembly-language
;; main(argc, argv, envp); call
push envp ;; rightmost argument
push argv ;;
push argc ;; leftmost argument ends up on top of stack
call main
pop ;; caller cleans up
pop
pop
У компіляторах, де такий випадок є умовою виклику, не потрібно робити нічого особливого для підтримки двох типів mainабо навіть додаткових видів. mainможе бути функцією без аргументів, і в цьому випадку він не звертає уваги на елементи, які були висунуті на стек. Якщо це функція від двох аргументів, тоді вона знаходить argcі argvяк два найвищі елементи стека. Якщо це варіант із трьома аргументами для платформи з покажчиком середовища (загальне розширення), це теж буде працювати: він знайде цей третій аргумент як третій елемент у верхній частині стека.
І тому фіксований дзвінок працює у всіх випадках, дозволяючи одному, фіксованому пусковому модулю бути зв’язаний з програмою. Цей модуль можна записати на мові C як функцію, схожу на цю:
/* I'm adding envp to show that even a popular platform-specific variant
can be handled. */
extern int main(int argc, char **argv, char **envp);
void __start(void)
{
/* This is the real startup function for the executable.
It performs a bunch of library initialization. */
/* ... */
/* And then: */
exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}
Іншими словами, цей стартовий модуль просто викликає три-аргумент main, завжди. Якщо main не приймає аргументів, або лише int, char **, це спрацьовує нормально, а також якщо не приймає аргументів, завдяки умовам виклику.
Якби ви робили подібні дії у своїй програмі, це було б не переносимим і вважалося б невизначеною поведінкою ISO C: декларування та виклик функції одним способом, а визначення іншим. Але трюк запуску компілятора не повинен бути портативним; він не керується правилами для портативних програм.
Але припустимо, що умови виклику такі, що це не може працювати таким чином. У такому випадку компілятор повинен обробляти mainспеціально. Коли він помічає, що він компілює mainфункцію, він може генерувати код, сумісний із, скажімо, викликом із трьох аргументів.
Тобто ви пишете це:
int main(void)
{
/* ... */
}
Але коли компілятор це бачить, він по суті виконує перетворення коду, так що функція, яку він компілює, виглядає приблизно так:
int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
/* ... */
}
за винятком того, що імена __argc_ignoreбуквально не існують. Жодні такі імена не вводяться у ваш обсяг, і не буде жодного попередження про невикористані аргументи. Перетворення коду змушує компілятор видавати код із правильним зв'язком, який знає, що він повинен очистити три аргументи.
Інша стратегія реалізації полягає в тому, щоб компілятор або, можливо, компонувальник створював __startфункцію на замовлення (або як би це ще не називалося), або принаймні вибрати одну з кількох попередньо скомпільованих альтернатив. В об'єктному файлі може зберігатися інформація про те, яка з підтримуваних форм mainвикористовується. Лінкер може переглянути цю інформацію та вибрати правильну версію модуля запуску, який містить виклик main, сумісний із визначенням програми. Реалізації C зазвичай мають лише невелику кількість підтримуваних форм, mainтому такий підхід здійсненний.
Компілятори для мови C99 завжди повинні обробляти mainспеціально, певною мірою, щоб підтримати хак, що якщо функція завершується без returnоператора, поведінка ніби return 0виконується. Це, знову ж таки, можна трактувати шляхом перетворення коду. Компілятор помічає, що функція, що викликається main, складається. Потім він перевіряє, чи є кінець тіла потенційно доступним. Якщо так, він вставляє areturn 0;
mainметод в одній програміC(або, справді, майже будь-якою мовою з такою конструкцією).