Согласование вызовов 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
вашему компилятору!