Прежде всего позвольте мне сказать, что это ужасный способ кодирования.
Тем не менее, давайте посмотрим, как это работает:
extern system, puts;
Это говорит о том, что system
и puts
определены где-то еще, и компоновщик позаботится о том, чтобы предоставить вам их адрес. Компилятор не знает, какой у них тип, поэтому gcc, по крайней мере, предполагает, что это int
, и выдает предупреждение. Я не могу найти в стандарте пункт, в котором говорится, является ли это поведение четко определенным, определяемым реализацией или неопределенным, но, безусловно, это плохо. Это особенно плохо, потому что эти символы не int
. Однако я нашел это (C11, Приложение J.2 - Неопределенное поведение):
Два объявления одного и того же объекта или функции указывают несовместимые типы (6.2.7).
Также существует вероятность того, что int
слишком мал для хранения указателя. Нет конца тому, насколько это плохо.
Итак, посмотрим в памяти, как это выглядит:
system (as 4-byte int) system (as function)
BYTE1 INSTRUCTION1
BYTE2 INSTRUCTION2
BYTE3 INSTRUCTION3
BYTE4 INSTRUCTION4
INSTRUCTION5
INSTRUCTION6
INSTRUCTION7
INSTRUCTION8
...
Итак, компоновщик делает адрес system
одинаковым для каждого скомпилированного объекта (эффективно связывая их вместе). Содержимое system
, с другой стороны, зависит от его типа, который компоновщик не знает или не заботится о нем.
Итак, в вашем файле, если сразу после:
extern system, puts;
ты пишешь:
printf("%d\n", system);
вы получите первую пару инструкций функции system
, как если бы это была int
, и распечатаете ее. Что плохо, и не делай этого.
С другой стороны, как я уже сказал, адрес system
, определенный как extern
в вашей программе, совпадает с реальной функцией system
. Итак, если взять адрес:
&system
и привести его к правильному типу:
(void(*)(char*))&system
вы получаете указатель на функцию, указывающий на реальную функцию system
. Обратите внимание, что system
только что было int
, и мы собираемся вызвать его как функцию, это само по себе неопределенное поведение:
C11 Приложение J-2 (неопределенное поведение):
Доступ к хранимому значению объекта осуществляется иначе, чем через lvalue допустимого типа (6.5).
C11 6,5 (выделено мной):
6. Эффективным типом объекта для доступа к его хранимому значению является объявленный тип объекта, если он есть. ...
7. Доступ к хранимому значению объекта должен осуществляться только выражением lvalue, которое имеет один из следующих типов: 88)
- тип, совместимый с эффективным типом объекта,
- квалифицированная версия типа, совместимая с действующим типом объекта,
- тип, который является подписанным или беззнаковым типом, соответствующим эффективному типу объекта,
- тип, который является подписанным или беззнаковым типом, соответствующим уточненной версии эффективного типа объекта,
- тип агрегата или объединения, который включает один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член подагрегата или содержащего объединения), или
- тип персонажа.
Пройдя все это, вы теперь имеете указатель на функцию, содержащий реальный адрес system
. Затем вы называете это в свое удовольствие. То же самое делается с puts
.
Это было плохо. Вот как это должно быть сделано на самом деле:
#include <stdlib.h>
#include <stdio.h>
int main(int argv,char **argc)
{
char buf[256];
/* system was unused */
strcpy(buf,argc[1]);
puts(argc[2]);
return 0; /* 0 means no error */
}
Помимо пользы от использования файлов заголовков, предоставленных реализацией, давайте посмотрим, как на самом деле работает этот.
Внутри файлов заголовков у вас есть много строк со многими объявлениями. Один для puts
может выглядеть так:
int puts(const char *s);
Во-первых, обратите внимание, как указатель на функцию в вашем коде на самом деле имеет неправильную сигнатуру функции? Это плохо. Это плохо, потому что при вызове функции void
вызывающей стороне не нужно предоставлять для нее пространство возвращаемого значения, в то время как puts
ожидает, что оно там будет, поэтому puts
запишет возвращаемое значение там, где он ожидает его найти, но на самом деле это пространство принадлежит чему-то другому. Это называется повреждением стека.
В любом случае, когда компилятор увидит такую строку:
int puts(const char *s);
- #P25# <блочная цитата> #P26# #P27# блочная цитата>
он будет знать, что это функция и какова ее подпись.
Таким образом, просто вызывая fputs(...)
, компилятор делает все правильно.
14.05.2014