2015-05-17 6 views
15

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

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

У вас здесь хороший момент! '8 (% ebp)' находится в кадре стека вызывающего, но это пространство, которое было выделено специально для передачи аргументов в 'foo'. Будет ли вызывающий абонент использовать это пространство для своих целей * после того, как '' foo' вернется, а не просто уничтожит его, отрегулировав указатель стека? Если это так, необходимо скопировать значение в стек стека 'foo'. Если нет, может быть безопасно «foo» «заимствовать» пространство в кадре стека вызывающего, а не копировать. Итак, чтобы узнать, хороша ли ваша идея или нет, вам нужно посмотреть, как выглядит код для вызывающего пользователя 'foo' *. –

+0

@AlexD Спасибо за ваш комментарий! Поскольку 'foo' может вызываться произвольной функцией, я думаю, что это вопрос о вызовах конвенций, а не в конкретном контексте, где вызывается' foo'. –

+1

Это интересный вопрос. Я нашел [этот другой вопрос] (http://stackoverflow.com/questions/1684682/c-calling-conventions-and-passed-arguments?rq=1), который утверждает, что gcc -O2 действительно изменил аргумент стоп-кода вызывающего абонента , – JS1

ответ

6

На самом деле, я только что собрали эту функцию с помощью 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 сделал именно то, что, по вашему мнению, должен был сделать, и использовал аргумент непосредственно там, где вызывающий его нажал на стек.

5

Обязательные аргументы by value. Поэтому любая модификация аргумента (например, 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 вашему компилятору!

+0

Поскольку пространство для аргументов выделяется декрементом '% esp', пространство для аргументов не пересекается с стековым фреймом вызывающего. Поэтому я думаю, что изменение аргументов в стеке не влияет на стеке кадра вызывающего. –

+0

Я не уверен, что последую за вашим мышлением (похоже, что вы нарушаете требование по требованию). Но не стесняйтесь предлагать патч для [Clang/LLVM] (http://clang.llvm.org/) или [GCC] (http://gcc.gnu.org/), если хотите. Это займет месяцы работы. –

+0

Вы должны отредактировать свой вопрос, чтобы улучшить его! –

12

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

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

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

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