Исходные данные: язык С, gcc4.4, x86, GNU/Linux
struct.h:
a.c:
b.c:
makefile:
Вопрос: что будет напечатано во время выполнения? Подумайте хотя бы минуту. А лучше возьмите отладчик и походите по этой нехитрой программе.
Ответ: заранее неизвестно, и всё благодаря тому, что в точке вызова функции f её прототип недоступен. При этом переданные параметры в точности соответствуют тому что ожидает вызываемая функция.
На моей системе, например, вывод таков:
Конечно, отсутствие прототипа в точке её вызова видно невооружённым глазом, но почему обычно безобидное «implicit declaration of function» имеет в данном случае такие тяжёлые последствия?
Всё благодаря ABI, которому следует gcc.
ABI определяет многие аспекты того, что в стандарте языка называется «implementation specific» или считается само собой разумеющимся. В частности, i386 ABI определяет вид машинного стека при вызове функции. Для функций возвращающих структуры/объединения предписывается следующее:
Как видно, по сравнению с обычным вызовом, добавляется еще один параметр, который снимает со стека вызываемая функция — адрес, по которому будет записано возвращаемое значение.
Что происходит в рассматриваемом случае (нарисовано, что поместила на стек вызывающая функция, что об этом думает вызываемая и что на стеке после возврата):
Налицо следующие аномалии:
Вот такая история.
Вместо заключения:
Предупреждения компилятора могут быть важнее, чем о них принято думать.
Знание ABI своей платформы иногда значительно облегчает жизнь.
А в x86_64 такого эффекта нет.
Приятного всем дня (:
struct.h:
struct S { int *a; int *b; };
a.c:
#include <stdio.h> #include "struct.h" struct S f(struct S v) { printf("v.a = %d, v.b = %d\n", *v.a, *v.b); return v; }
b.c:
#include <stdio.h> #include "struct.h" int main() { int a = 1, b = 2; struct S v = {&a, &b}; f(v); printf("a = %d, b = %d\n", a, b); return 0; }
makefile:
all: test ./test test: a.c b.c struct.h gcc a.c b.c -g -o test
Вопрос: что будет напечатано во время выполнения? Подумайте хотя бы минуту. А лучше возьмите отладчик и походите по этой нехитрой программе.
Ответ: заранее неизвестно, и всё благодаря тому, что в точке вызова функции f её прототип недоступен. При этом переданные параметры в точности соответствуют тому что ожидает вызываемая функция.
На моей системе, например, вывод таков:
v.a = 2, v.b = 0 a = 8559808, b = -2398008
Конечно, отсутствие прототипа в точке её вызова видно невооружённым глазом, но почему обычно безобидное «implicit declaration of function» имеет в данном случае такие тяжёлые последствия?
Всё благодаря ABI, которому следует gcc.
ABI определяет многие аспекты того, что в стандарте языка называется «implementation specific» или считается само собой разумеющимся. В частности, i386 ABI определяет вид машинного стека при вызове функции. Для функций возвращающих структуры/объединения предписывается следующее:
Позиция После вызова После возврата Позиция 4n+4(%esp) | слово n | | слово n | 4n-4(%esp) | ... | | ... | 8(%esp) | слово 1 | | слово 1 | 0(%esp) |------------| |--------------| 4(%esp) | адрес | | не определено| | результата | | | |------------| | | 0(%esp) | адрес | | | | возврата | | | |------------| |--------------|
Как видно, по сравнению с обычным вызовом, добавляется еще один параметр, который снимает со стека вызываемая функция — адрес, по которому будет записано возвращаемое значение.
Что происходит в рассматриваемом случае (нарисовано, что поместила на стек вызывающая функция, что об этом думает вызываемая и что на стеке после возврата):
Вызывающая Вызываемая Сразу функция функция после возврата (ожидала) 16(%esp) | локальные | | | | локальные | | | |------------| | | 12(%esp) | переменные | | v.b | | переменные | |============| | | |--------------| 8(%esp) | v.b | | v.a | | v.b | 0(%esp) |------------| |------------| |==============| 4(%esp) | v.a | | адрес | | не определено| | | | результата | | | |============| |------------| | | 0(%esp) | адрес | | адрес | | | | возврата | | возврата | | | |------------| |------------| |--------------|
Налицо следующие аномалии:
- вызываемая функция видит параметры со сдвигом на 1 (v.a «внутри» = v.b «снаружи»);
- вызываемая функция затирает своим возвращаемым значением память по адресу, численно равному значению первого аргумента (мусор в v.a);
- указатель стека после возврата смещён на 1 слово назад. Очень плохо, если учесть, что компилятор рассчитывает найти его неизменным, найти неизменными параметры, которые он поместил на стек, может обращаться к локальным переменным по смещениям от esp и попытается выполнить очистку прибавив к esp 8.
Вот такая история.
Вместо заключения:
Предупреждения компилятора могут быть важнее, чем о них принято думать.
Знание ABI своей платформы иногда значительно облегчает жизнь.
А в x86_64 такого эффекта нет.
Приятного всем дня (: