Как использовать метки в GCC Inline Assembly?
Я пытаюсь изучить встроенную сборку x86-64 и решил реализовать этот очень простой метод подкачки, который просто упорядочивает a
а также b
в порядке возрастания:
#include <stdio.h>
void swap(int* a, int* b)
{
asm(".intel_syntax noprefix");
asm("mov eax, DWORD PTR [rdi]");
asm("mov ebx, DWORD PTR [rsi]");
asm("cmp eax, ebx");
asm("jle .L1");
asm("mov DWORD PTR [rdi], ebx");
asm("mov DWORD PTR [rsi], eax");
asm(".L1:");
asm(".att_syntax noprefix");
}
int main()
{
int input[3];
scanf("%d%d%d", &input[0], &input[1], &input[2]);
swap(&input[0], &input[1]);
swap(&input[1], &input[2]);
swap(&input[0], &input[1]);
printf("%d %d %d\n", input[0], input[1], input[2]);
return 0;
}
Приведенный выше код работает, как и ожидалось, когда я запускаю его с этой командой:
> gcc main.c
> ./a.out
> 3 2 1
> 1 2 3
Однако, как только я включаю оптимизацию, я получаю следующие сообщения об ошибках:
> gcc -O2 main.c
> main.c: Assembler messages:
> main.c:12: Error: symbol `.L1' is already defined
> main.c:12: Error: symbol `.L1' is already defined
> main.c:12: Error: symbol `.L1' is already defined
Если я правильно понял, это потому, что gcc
пытается вставить мой swap
функция, когда оптимизация включена, вызывая метку .L1
быть определенным несколько раз в файле сборки.
Я пытался найти ответ на эту проблему, но ничего не получается. В этом часто задаваемом вопросе предлагается вместо этого использовать локальные метки, и я тоже попробовал это:
#include <stdio.h>
void swap(int* a, int* b)
{
asm(".intel_syntax noprefix");
asm("mov eax, DWORD PTR [rdi]");
asm("mov ebx, DWORD PTR [rsi]");
asm("cmp eax, ebx");
asm("jle 1f");
asm("mov DWORD PTR [rdi], ebx");
asm("mov DWORD PTR [rsi], eax");
asm("1:");
asm(".att_syntax noprefix");
}
Но при попытке запустить программу я теперь получаю ошибку сегментации:
> gcc -O2 main.c
> ./a.out
> 3 2 1
> Segmentation fault
Я также попробовал предлагаемое решение этого часто задаваемого вопроса и изменил название .L1
в CustomLabel1
на случай, если произойдет конфликт имен, но он все равно выдаст мне старую ошибку:
> gcc -O2 main.c
> main.c: Assembler messages:
> main.c:12: Error: symbol `CustomLabel1' is already defined
> main.c:12: Error: symbol `CustomLabel1' is already defined
> main.c:12: Error: symbol `CustomLabel1' is already defined
Наконец я также попробовал это предложение:
void swap(int* a, int* b)
{
asm(".intel_syntax noprefix");
asm("mov eax, DWORD PTR [rdi]");
asm("mov ebx, DWORD PTR [rsi]");
asm("cmp eax, ebx");
asm("jle label%=");
asm("mov DWORD PTR [rdi], ebx");
asm("mov DWORD PTR [rsi], eax");
asm("label%=:");
asm(".att_syntax noprefix");
}
Но тогда я получаю эти ошибки вместо:
main.c: Assembler messages:
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
main.c:9: Error: invalid character '=' in operand 1
main.c:12: Error: invalid character '%' in mnemonic
Итак, мой вопрос:
Как я могу использовать метки во встроенной сборке?
Это результат дизассемблирования для оптимизированной версии:
> gcc -O2 -S main.c
.file "main.c"
.section .text.unlikely,"ax",@progbits
.LCOLDB0:
.text
.LHOTB0:
.p2align 4,,15
.globl swap
.type swap, @function
swap:
.LFB23:
.cfi_startproc
#APP
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
#NO_APP
ret
.cfi_endproc
.LFE23:
.size swap, .-swap
.section .text.unlikely
.LCOLDE0:
.text
.LHOTE0:
.section .rodata.str1.1,"aMS",@progbits,1
.LC1:
.string "%d%d%d"
.LC2:
.string "%d %d %d\n"
.section .text.unlikely
.LCOLDB3:
.section .text.startup,"ax",@progbits
.LHOTB3:
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB24:
.cfi_startproc
subq $40, %rsp
.cfi_def_cfa_offset 48
movl $.LC1, %edi
movq %fs:40, %rax
movq %rax, 24(%rsp)
xorl %eax, %eax
leaq 8(%rsp), %rcx
leaq 4(%rsp), %rdx
movq %rsp, %rsi
call __isoc99_scanf
#APP
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
# 5 "main.c" 1
.intel_syntax noprefix
# 0 "" 2
# 6 "main.c" 1
mov eax, DWORD PTR [rdi]
# 0 "" 2
# 7 "main.c" 1
mov ebx, DWORD PTR [rsi]
# 0 "" 2
# 8 "main.c" 1
cmp eax, ebx
# 0 "" 2
# 9 "main.c" 1
jle 1f
# 0 "" 2
# 10 "main.c" 1
mov DWORD PTR [rdi], ebx
# 0 "" 2
# 11 "main.c" 1
mov DWORD PTR [rsi], eax
# 0 "" 2
# 12 "main.c" 1
1:
# 0 "" 2
# 13 "main.c" 1
.att_syntax noprefix
# 0 "" 2
#NO_APP
movl 8(%rsp), %r8d
movl 4(%rsp), %ecx
movl $.LC2, %esi
movl (%rsp), %edx
xorl %eax, %eax
movl $1, %edi
call __printf_chk
movq 24(%rsp), %rsi
xorq %fs:40, %rsi
jne .L6
xorl %eax, %eax
addq $40, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L6:
.cfi_restore_state
call __stack_chk_fail
.cfi_endproc
.LFE24:
.size main, .-main
.section .text.unlikely
.LCOLDE3:
.section .text.startup
.LHOTE3:
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
3 ответа
Существует множество учебных пособий - включая этот (возможно, лучший из известных мне) и некоторую информацию о модификаторах размера операнда.
Вот первая реализация - swap_2
:
void swap_2 (int *a, int *b)
{
int tmp0, tmp1;
__asm__ volatile (
"movl (%0), %k2\n\t" /* %2 (tmp0) = (*a) */
"movl (%1), %k3\n\t" /* %3 (tmp1) = (*b) */
"cmpl %k3, %k2\n\t"
"jle %=f\n\t" /* if (%2 <= %3) (at&t!) */
"movl %k3, (%0)\n\t"
"movl %k2, (%1)\n\t"
"%=:\n\t"
: "+r" (a), "+r" (b), "=r" (tmp0), "=r" (tmp1) :
: "memory" /* "cc" */ );
}
Несколько заметок:
volatile
(или же__volatile__
) требуется, так как компилятор только "видит"(a)
а также(b)
(и не "знает", что вы потенциально обмениваетесь их содержанием), и в противном случае был бы свободен для оптимизации всегоasm
выпискаtmp0
а такжеtmp1
в противном случае будет также рассматриваться как неиспользуемые переменные."+r"
означает, что это и вход, и выход, которые могут быть изменены; только не в этом случае, и они могут быть строго введены только - об этом чуть позже...Суффикс 'l' в 'movl' на самом деле не нужен; также нет модификатора длины 'k' (32-бит) для регистров. Поскольку вы используете Linux (ELF) ABI,
int
32 бита для интерфейсов IA32 и x86-64.%=
токен генерирует для нас уникальный ярлык. Кстати, синтаксис перехода<label>f
означает прыжок вперед, и<label>b
значит обратно.Для правильности нам нужно
"memory"
поскольку компилятор не может знать, были ли изменены значения из разыменованных указателей. Это может быть проблемой в более сложном встроенном ассемблере, окруженном кодом C, так как он делает недействительными все хранящиеся в настоящий момент значения в памяти - и часто это подход кувалдой. Появление в конце функции таким образом, это не будет проблемой - но вы можете прочитать больше об этом здесь (см.: Clobbers)"cc"
флаги регистрации clobber подробно описаны в том же разделе. на x86 он ничего не делает. Некоторые авторы включают это для ясности, но так как практически все нетривиальноasm
операторы влияют на регистр флагов, просто предполагается, что он по умолчанию закрыт.
Вот реализация C - swap_1
:
void swap_1 (int *a, int *b)
{
if (*a > *b)
{
int t = *a; *a = *b; *b = t;
}
}
Компилирование с gcc -O2
для x86-64 ELF я получаю идентичный код. Просто немного удачи, которую выбрал компилятор tmp0
а также tmp1
использовать те же самые свободные регистры для темпов... вырезание шума, как директивы.cfi и т. д., дает:
swap_2:
movl (%rdi), %eax
movl (%rsi), %edx
cmpl %edx, %eax
jle 21f
movl %edx, (%rdi)
movl %eax, (%rsi)
21:
ret
Как указано, swap_1
код был идентичен, за исключением того, что компилятор выбрал .L1
для его метки прыжка. Компиляция кода с -m32
сгенерировал один и тот же код (кроме использования регистров tmp в другом порядке). Это приводит к дополнительным расходам, так как интерфейс ELI IA32 ELF передает параметры в стек, в то время как интерфейс x86-64 передает первые два параметра в %rdi
а также %rsi
соответственно.
Лечение (a)
а также (b)
только для ввода - swap_3
:
void swap_3 (int *a, int *b)
{
int tmp0, tmp1;
__asm__ volatile (
"mov (%[a]), %[x]\n\t" /* x = (*a) */
"mov (%[b]), %[y]\n\t" /* y = (*b) */
"cmp %[y], %[x]\n\t"
"jle %=f\n\t" /* if (x <= y) (at&t!) */
"mov %[y], (%[a])\n\t"
"mov %[x], (%[b])\n\t"
"%=:\n\t"
: [x] "=&r" (tmp0), [y] "=&r" (tmp1)
: [a] "r" (a), [b] "r" (b) : "memory" /* "cc" */ );
}
Я покончил с суффиксом 'l' и модификаторами 'k', потому что они не нужны. Я также использовал синтаксис "символического имени" для операндов, поскольку он часто помогает сделать код более читабельным.
(a)
а также (b)
теперь действительно регистры только для ввода. Так что же "=&r"
синтаксис значит? &
обозначает ранний операнд Clobber. В этом случае значение может быть записано до того, как мы закончим использовать входные операнды, и поэтому компилятор должен выбрать регистры, отличные от регистров, выбранных для входных операндов.
Еще раз, компилятор генерирует идентичный код, как это было для swap_1
а также swap_2
,
Я написал гораздо больше, чем планировал в этом ответе, но, как вы можете видеть, очень сложно поддерживать осведомленность обо всей информации, которую должен знать компилятор, а также об особенностях каждого набора команд (ISA) и ABI.
Вы не можете просто положить кучу asm
заявления встроены, как это. Оптимизатор может переупорядочивать, дублировать и удалять их в зависимости от того, какие ограничения он знает. (В вашем случае, он ничего не знает.)
Итак, во-первых, вы должны консолидировать asm вместе с надлежащими ограничениями чтения / записи / clobber. Во-вторых, есть особый asm goto
форма, которая дает сборку с метками уровня C.
void swap(int *a, int *b) {
int tmp1, tmp2;
asm(
"mov (%2), %0\n"
"mov (%3), %1\n"
: "=r" (tmp1), "=r" (tmp2)
: "r" (a), "r" (b)
);
asm goto(
"cmp %1, %0\n"
"jle %l4\n"
"mov %1, (%2)\n"
"mov %0, (%3)\n"
:
: "r" (tmp1), "r" (tmp2), "r" (a), "r" (b)
: "cc", "memory"
: L1
);
L1:
return;
}
Вы не можете предполагать, что значения находятся в каком-либо конкретном регистре в вашем ассемблерном коде - вам нужно использовать ограничения, чтобы сообщить gcc, какие значения вы хотите прочитать и записать, и заставить его сообщить вам, в каком регистре они находятся. Документы gcc сообщают вам большинство что нужно знать, но довольно плотно. Есть также учебники, которые вы можете легко найти с помощью веб-поиска ( здесь или здесь)