Pull to refresh

О неявных объявлениях, обратной совместимости и ABI

Reading time2 min
Views2.2K
Исходные данные: язык С, gcc4.4, x86, GNU/Linux

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 такого эффекта нет.

Приятного всем дня (:
Tags:
Hubs:
Total votes 58: ↑46 and ↓12+34
Comments78

Articles