Разлив регистра аргументов gcc на x86-64
Я экспериментирую со сборкой x86-64. Скомпилировав эту фиктивную функцию:
long myfunc(long a, long b, long c, long d,
long e, long f, long g, long h)
{
long xx = a * b * c * d * e * f * g * h;
long yy = a + b + c + d + e + f + g + h;
long zz = utilfunc(xx, yy, xx % yy);
return zz + 20;
}
С gcc -O0 -g
Я был удивлен, обнаружив следующее в начале сборки функции:
0000000000400520 <myfunc>:
400520: 55 push rbp
400521: 48 89 e5 mov rbp,rsp
400524: 48 83 ec 50 sub rsp,0x50
400528: 48 89 7d d8 mov QWORD PTR [rbp-0x28],rdi
40052c: 48 89 75 d0 mov QWORD PTR [rbp-0x30],rsi
400530: 48 89 55 c8 mov QWORD PTR [rbp-0x38],rdx
400534: 48 89 4d c0 mov QWORD PTR [rbp-0x40],rcx
400538: 4c 89 45 b8 mov QWORD PTR [rbp-0x48],r8
40053c: 4c 89 4d b0 mov QWORD PTR [rbp-0x50],r9
400540: 48 8b 45 d8 mov rax,QWORD PTR [rbp-0x28]
400544: 48 0f af 45 d0 imul rax,QWORD PTR [rbp-0x30]
400549: 48 0f af 45 c8 imul rax,QWORD PTR [rbp-0x38]
40054e: 48 0f af 45 c0 imul rax,QWORD PTR [rbp-0x40]
400553: 48 0f af 45 b8 imul rax,QWORD PTR [rbp-0x48]
400558: 48 0f af 45 b0 imul rax,QWORD PTR [rbp-0x50]
40055d: 48 0f af 45 10 imul rax,QWORD PTR [rbp+0x10]
400562: 48 0f af 45 18 imul rax,QWORD PTR [rbp+0x18]
gcc
очень странно проливает все регистры аргументов в стек, а затем забирает их из памяти для дальнейших операций.
Это происходит только на -O0
(с -O1
проблем нет) но все же зачем? Это выглядит как антиоптимизация для меня - почему бы gcc
сделай это?
2 ответа
Я ни в коем случае не эксперт по внутренним компонентам GCC, но я сделаю это. К сожалению, большая часть информации о распределении и распределении регистров GCC, кажется, устарела (ссылаясь на такие файлы, как local-alloc.c
которые больше не существуют).
Я смотрю на исходный код gcc-4.5-20110825
,
В GNU C Compiler Internals упоминается, что исходный код функции генерируется expand_function_start
в gcc/function.c
, Там мы находим следующее для обработки параметров:
4462 /* Initialize rtx for parameters and local variables.
4463 In some cases this requires emitting insns. */
4464 assign_parms (subr);
В assign_parms
код, который обрабатывает место хранения каждого аргумента, выглядит следующим образом:
3207 if (assign_parm_setup_block_p (&data))
3208 assign_parm_setup_block (&all, parm, &data);
3209 else if (data.passed_pointer || use_register_for_decl (parm))
3210 assign_parm_setup_reg (&all, parm, &data);
3211 else
3212 assign_parm_setup_stack (&all, parm, &data);
assign_parm_setup_block_p
обрабатывает агрегированные типы данных и не применяется в этом случае и поскольку данные не передаются в качестве указателя GCC проверяет use_register_for_decl
,
Здесь соответствующая часть:
1972 if (optimize)
1973 return true;
1974
1975 if (!DECL_REGISTER (decl))
1976 return false;
DECL_REGISTER
проверяет, была ли переменная объявлена с register
ключевое слово. И теперь у нас есть ответ: большинство параметров живут в стеке, когда оптимизация не включена, а затем обрабатываются assign_parm_setup_stack
, Маршрут, пройденный через исходный код до того, как он закончится разливом значения, немного сложнее для аргументов указателя, но может быть отслежен в том же файле, если вам интересно.
Почему GCC выдает все аргументы и локальные переменные с отключенной оптимизацией? Чтобы помочь отладке. Рассмотрим эту простую функцию:
1 extern int bar(int);
2 int foo(int a) {
3 int b = bar(a | 1);
4 b += 42;
5 return b;
6 }
Составлено с gcc -O1 -c
это генерирует следующее на моей машине:
0: 48 83 ec 08 sub $0x8,%rsp
4: 83 cf 01 or $0x1,%edi
7: e8 00 00 00 00 callq c <foo+0xc>
c: 83 c0 2a add $0x2a,%eax
f: 48 83 c4 08 add $0x8,%rsp
13: c3 retq
Что хорошо, кроме случаев, когда вы разбиваете строку 5 и пытаетесь напечатать значение a, вы получаете
(gdb) print a
$1 = <value optimized out>
Поскольку аргумент перезаписывается, так как он не используется после вызова bar
,
Пара причин:
- В общем случае аргумент функции должен рассматриваться как локальная переменная, потому что он может быть сохранен или иметь свой адрес внутри функции. Поэтому проще всего просто выделить слот стека для каждого аргумента.
- Отладочная информация становится намного проще для вывода в стеке: значение аргумента всегда находится в каком-то определенном месте, а не перемещается между регистрами и памятью.
Когда вы смотрите на код -O0 в целом, учтите, что главные приоритеты компилятора - максимально сократить время компиляции и генерировать высококачественную отладочную информацию.