Согласование вызовов x86: должны ли аргументы, передаваемые стеком, только для чтения?

Кажется, что современные компиляторы трактуют аргументы, переданные стеком, только для чтения. Обратите внимание, что в соглашении на вызов x86 вызывающий пользователь вставляет аргументы в стек, а вызываемый использует аргументы в стеке. Например, следующий код C:

extern int goo(int *x);
int foo(int x, int y) {
  goo(&x);
  return x;
}

скомпилирован clang -O3 -c g.c -S -m32 в OS X 10.10 в:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 10
    .globl  _foo
    .align  4, 0x90
_foo:                                   ## @foo
## BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    movl    8(%ebp), %eax
    movl    %eax, -4(%ebp)
    leal    -4(%ebp), %eax
    movl    %eax, (%esp)
    calll   _goo
    movl    -4(%ebp), %eax
    addl    $8, %esp
    popl    %ebp
    retl


.subsections_via_symbols

Здесь параметр x (8(%ebp)) сначала загружается в %eax; и затем сохраняется в -4(%ebp); и адрес -4(%ebp) сохраняется в %eax; и %eax передается функции goo.

Интересно, почему Clang генерирует код, который копирует значение, хранящееся в 8(%ebp), в -4(%ebp), а не просто передает адрес 8(%ebp) функции goo. Это позволит сэкономить память и повысить производительность. Я наблюдал подобное поведение в GCC тоже (под OS X). Чтобы быть более конкретным, мне интересно, почему компиляторы не генерируют:

  .section  __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 10
    .globl  _foo
    .align  4, 0x90
_foo:                                   ## @foo
## BB#0:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    leal    8(%ebp), %eax
    movl    %eax, (%esp)
    calll   _goo
    movl    8(%ebp), %eax
    addl    $8, %esp
    popl    %ebp
    retl


.subsections_via_symbols

Я искал документы, если соглашение о вызове x86 требует, чтобы переданные аргументы были доступны только для чтения, но я не смог найти ничего в этом вопросе. Кто-нибудь думает по этому поводу?

Ответы

Ответ 1

Собственно, я просто скомпилировал эту функцию с помощью GCC:

int foo(int x)
{
    goo(&x);
    return x;
}

И он сгенерировал этот код:

_foo:
        pushl       %ebp
        movl        %esp, %ebp
        subl        $24, %esp
        leal        8(%ebp), %eax
        movl        %eax, (%esp)
        call        _goo
        movl        8(%ebp), %eax
        leave
        ret

Это использует GCC 4.9.2 (по 32-разрядному cygwin, если это имеет значение), без оптимизации. Таким образом, GCC сделал именно то, что, по вашему мнению, должен был сделать, и использовал аргумент непосредственно там, где вызывающий его нажал на стек.

Ответ 2

Правила для C - это то, что параметры должны передаваться по значению. Компилятор преобразует с одного языка (с одним набором правил) на другой язык (потенциально с совершенно другим набором правил). Единственное ограничение заключается в том, что поведение остается неизменным. Правила языка C не применяются к целевому языку (например, сборке).

Это означает, что если компилятор чувствует себя подобно генерации языка ассемблера, где параметры передаются по ссылке и не передаются по значению; то это совершенно законно (пока поведение остается прежним).

Реальное ограничение не имеет никакого отношения к C вообще. Реальное ограничение связано. Чтобы разные объектные файлы могли быть связаны друг с другом, необходимы стандарты, чтобы гарантировать, что независимо от того, какой вызывающий объект в одном объектном файле ожидает совпадений, независимо от того, какой вызов вызывается в другом объектном файле. Это то, что известно как ABI. В некоторых случаях (например, 64-битный 80x86) для одной и той же архитектуры существует несколько различных ABI.

Вы даже можете изобрести свой собственный ABI, который радикально отличается (и реализует ваши собственные инструменты, которые поддерживают ваш собственный радикально отличающийся ABI), и который совершенно легален в отношении стандартов C; даже если ваш ABI требует "пройти по ссылке" для всего (пока поведение остается прежним).

Ответ 3

язык программирования C определяет, что аргументы передаются по значению. Таким образом, любая модификация аргумента (например, x++; как первая инструкция вашего foo) является локальной для этой функции и не распространяется на вызывающего.

Следовательно, общий вызов должен требовать копирования аргументов на каждом сайте вызова. Вызывающие соглашения должны быть достаточно общими для неизвестных вызовов, например. через указатель функции!

Конечно, если вы передаете адрес в некоторую зону памяти, вызываемая функция может разыменовать этот указатель, например. как в

int goo(int *x) {
    static int count;
    *x = count++;
    return count % 3;
}

BTW, вы можете использовать оптимизацию времени соединения (путем компиляции и связывания с clang -flto -O2 или gcc -flto -O2), чтобы, возможно, включить компилятор для улучшения или встроить некоторые вызовы между единицами перевода.

Обратите внимание, что Clang/LLVM и GCC являются компиляторами free software. Не стесняйтесь предлагать исправление для них, если хотите (но поскольку оба являются очень сложными компонентами программного обеспечения, вам нужно будет работать несколько месяцев, чтобы сделать этот патч).

NB. При просмотре полученного кода сборки передайте -fverbose-asm вашему компилятору!