Исходные данные: язык С, 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 такого эффекта нет.
Приятного всем дня (:
