Доброго! Ассемблер в общем не знаю, но код, который генерирует gcc иногда посматриваю, ради расширения кругозора. Попробовал сделать такие вставки в код (количество инструкций, кстати, если генерировать gcc с опцией -S, увеличилось). Но интересно, будет ли работать на другой машине такой код? (синтаксис, как я понял, AT&T). Какие регистры дёргать, подбирал, скажем так, методом тыка. Онлайн компиляторы запускают. Машина у меня Atom 64b с Linux. Код (Text): /*Порождение перестановок*/ #include <stdio.h> int main(){ char a[] = "4321"; /*array*/ int i, j, f; asm("movl $24, -20(%rbp);"); /*f=24*/ char c; while (f--) { printf("%s\n", a); asm ("movl $1, -28(%rbp);"); /*i=1*/ while(a[i] >= a[i-1]) i++; asm ("movl $0, -24(%rbp);"); /*j=0*/ while(a[j] <= a[i])j++; c=a[j]; a[j]=a[i]; a[i]=c; i--; for (j = 0; j < i; i--, j++) { c = a[i]; a[i] = a[j]; a[j] = c; } } }
Скомпилированый - будет работать на другой машине той же архитектуры, архитектура у вас x64. Ну и ОС должна позволять запускать ваш elf. Сишный будет работать если: 1) машина x64 2) компилятор поддерживает синтаксис 3) компилятор так же распологает на стеке локальные переменные, тот же пролог процедуры и т.д. Кстати возможно ваш код перестанет работать, если собрать с другими опциями компилятора, т.к. могут измениться соглашения, расположение и выравнивание переменных и прочая.
Благодаю. Понял. Примерно так и предположил. Ведь, как я понял, я напрямую изменяю значение стека. Разница в скорости, как я понимаю, тоже не будет если оставить i=1 или вставку на Ассемблере. Или будет?! В общем у такого кода слабая переносимость, но если надо немножко запутать код, то как раз.
Ты же говоришь, что смотришь выхлоп гцц. И как, без -O3 там совсем не треш и угар получается? ЗЫ: когда-то развлекался с гцц, было потешно. Оказалось без -O3 он совсем плох и даже с ним не очень.
Эти вставки плохо оптимизируются, по мне или полностью на ассемблере, или лучше без него, ну может отдельно в dll запихнуть свой код на асме.
Разница очень большая по количеству строк. С -O3 всего 37 строк. Без этой опции 121 строка. Это если применять к коду с встаками на Асме. Если оставить присвоение переменным на C, то чистый с код с опцией -O3 выдаёт 134 строки. P.S. Если сокращение количества строк кода считать оптимизацией (я почему-то верю, что так и надо считать. Или я не прав?! (ибо количество операций и время)), то эти вставки имеют смысл, код сократился почти в три раза. Всем большое спасибо!
Количество строк не показатель, там же бред местами получается, если разбирать. Такая же портянка из паттернов, как у деда Борланда.
C -O3 и вставками: Код (Text): .file "perm_asm.c" .text .section .text.startup,"ax",@progbits .p2align 4 .globl main .type main, @function main: .LFB23: .cfi_startproc subq $24, %rsp .cfi_def_cfa_offset 32 movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax movl $825373492, 3(%rsp) movb $0, 7(%rsp) #APP # 6 "perm_asm.c" 1 movl $24, -20(%rbp); # 0 "" 2 #NO_APP movq 8(%rsp), %rax subq %fs:40, %rax jne .L24 xorl %eax, %eax addq $24, %rsp .cfi_remember_state .cfi_def_cfa_offset 8 ret .L24: .cfi_restore_state call __stack_chk_fail@PLT .cfi_endproc .LFE23: .size main, .-main .ident "GCC: (Gentoo 11.3.0 p7) 11.3.0" .section .note.GNU-stack,"",@progbits Без -O3 и без вставок: Код (Text): .file "perm.c" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $32, %rsp movq %fs:40, %rax movq %rax, -8(%rbp) xorl %eax, %eax movl $825373492, -13(%rbp) movb $0, -9(%rbp) movl $24, -20(%rbp) jmp .L2 .L9: leaq -13(%rbp), %rax movq %rax, %rdi call puts@PLT movl $1, -28(%rbp) jmp .L3 .L4: addl $1, -28(%rbp) .L3: movl -28(%rbp), %eax cltq movzbl -13(%rbp,%rax), %edx movl -28(%rbp), %eax subl $1, %eax cltq movzbl -13(%rbp,%rax), %eax cmpb %al, %dl jge .L4 movl $0, -24(%rbp) jmp .L5 .L6: addl $1, -24(%rbp) .L5: movl -24(%rbp), %eax cltq movzbl -13(%rbp,%rax), %edx movl -28(%rbp), %eax cltq movzbl -13(%rbp,%rax), %eax cmpb %al, %dl jle .L6 movl -24(%rbp), %eax cltq movzbl -13(%rbp,%rax), %eax movb %al, -29(%rbp) movl -28(%rbp), %eax cltq movzbl -13(%rbp,%rax), %edx movl -24(%rbp), %eax cltq movb %dl, -13(%rbp,%rax) movl -28(%rbp), %eax cltq movzbl -29(%rbp), %edx movb %dl, -13(%rbp,%rax) subl $1, -28(%rbp) movl $0, -24(%rbp) jmp .L7 .L8: movl -28(%rbp), %eax cltq movzbl -13(%rbp,%rax), %eax movb %al, -29(%rbp) movl -24(%rbp), %eax cltq movzbl -13(%rbp,%rax), %edx movl -28(%rbp), %eax cltq movb %dl, -13(%rbp,%rax) movl -24(%rbp), %eax cltq movzbl -29(%rbp), %edx movb %dl, -13(%rbp,%rax) subl $1, -28(%rbp) addl $1, -24(%rbp) .L7: movl -24(%rbp), %eax cmpl -28(%rbp), %eax jl .L8 .L2: movl -20(%rbp), %eax leal -1(%rax), %edx movl %edx, -20(%rbp) testl %eax, %eax jne .L9 movl $0, %eax movq -8(%rbp), %rdx subq %fs:40, %rdx je .L11 call __stack_chk_fail@PLT .L11: leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Gentoo 11.3.0 p7) 11.3.0" .section .note.GNU-stack,"",@progbits P.S. Кстати, nasm не смог собрать первый код. Куча вот таких ошибок: Код (Text): perm_asm.asm:2: warning: label alone on a line without a colon might be in error [-w+label-orphan] perm_asm.asm:3: error: parser: instruction expected perm_asm.asm:4: error: parser: instruction expected perm_asm.asm:5: error: parser: instruction expected perm_asm.asm:6: error: parser: instruction expected
А куда у тебя вся полезная нагрузка с -O3 делась? Только f инициализируется, дальше просто убирается SEH и выход. Код (Text): movq %fs:40, %rax movq %rax, 8(%rsp) xorl %eax, %eax movl $825373492, 3(%rsp) movb $0, 7(%rsp) #APP # 6 "perm_asm.c" 1 movl $24, -20(%rbp); # 0 "" 2 #NO_APP movq 8(%rsp), %rax subq %fs:40, %rax jne .L24 xorl %eax, %eax addq $24, %rsp .cfi_remember_state .cfi_def_cfa_offset 8 ret .L24:
Вот и выяснилось! Рано я обрадовался оптимизации! И -O3, и -O2 дают на выходе segfault. Оптимизировал всё, в буквальном смысле.
Я хз можно ли, не можно, но лучше используй не адрес переменной, а именно ее метку во вставках. Похоже оптимизация убрала все лишнее и неиспользуемое.
Переход в смысле? Код (Text): jmp .L3 К примеру? Не получится в случае с этим кодом, скорее всего. Или что-то другое имеется в виду?
В ассемблерных вставках в гцц есть списки зависимых переменных, оптимизатор цэ удаляет неиспользуемые переменные совершенно логичным образом до уровня ассемблера, очень большое количество оптимизаций происходит на уровне SSA-представления.
В смысле имя переменной f использовать, а не адресовать по rbp. Либо найти способ директивами нужные переменные вручную пометить как используемые.
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html - можно указать входные переменные, выходные переменный, регистры, которые вставка изменила и метки.
Можно ещё безо всяких вставок взять адрес любой переменной на стеке и по смещениям модифицировать что угодно, включая адрес возврата. Только надо учитывать выравнивания.
Правильно пользоваться ассемблерными вставками на GCC - это немножечко искусство. GCC вообще по меньшей мере исторически устроен как бы так, что генерирует не объектный код (.o), а исходник для GNU Assembler (.s) и вот он уже компилируется в .o. Эту фазу, однако, давно уже запрятали под капот и её наличие даже никак не отсвечивает пока не дашь ключ -S. Но под капотом оно всё так и осталось, в этом легко убедится как раз скомпилировав полученный .s - результирующий бинарник будет до байта совпадать с тем что получается и без -S. В результате получается интересная фигня - если gcc видит в исходнике inline asm, то он собственно никак не компилирует его, а просто вставляет его текст в ассемблерный код который нагенерировал сам, и отдаёт уже ассемблеру (GNU ASM) всё полученное - если будут ошибки в тексте, то о них тебе будет сообщать уже GNU Asm, а с ключом -S можно будет легко увидеть сущность происходящей текстоподстановки. Казалось бы такая тупая и прямолинейная техника должна быть неудобной или негибкой, но удивительно насколько всё обстоит прямо противоположным образом - то как в gcc реализован inline asm напротив даёт огромный спектр возможностей и оптимизационного потенциала. Удивительно, но ему нередко проигрывает в этом встроенный в MSVC 32 асм MS, несмотря на то, что он бесшовно интегрирован в синтаксис языка. Однако при всём при этом поначалу и зачастую будет возникать ощущение, что способ GCC негибок и чего то в нём не получается сделать, ибо вот ну не хватает возможностей. Это ощущение ложно - нужно или еще хорошенько подумать или спросить совета на форуме. Как правило решение находится на поверхности, просто руководства явно в него не тыкают и оно, поэтому, не отсвечивает своей очевидностью. Искусство тут крутится вокруг того чтобы правильно "сшить" свою текстовую асмоподстановку с окружающей программой через параметры clobber list и т.п. Мы должны явно сообщить компилятору какие у нас входные данные, какие у нас выходные данные и какие вещи в процессе нашего ассемблера будут испорчены и тогда он сам раскидает всё наилучшим образом, а главное - не даст выстрелить себе в ногу. При этом есть всякие нюансы - например не стоит в самом начале своего асмокода грузить в регистр переменную - это плохой и негибкий вариант. Надо через параметры асмоподстановки сообщить компилятору, что тебе нужно чтобы содержимое переменной оказалось в этом регистре - тогда он максимально оптимально сошьёт код так что в оптимизированном варианте программе расчёты с этой переменной уже к моменту асмоподстановки велись в этом регистре и не будет вообще никакого пенальти на перекладывания. Если просто документацию почитать, то куча таких моментов просто неочевидна и можно городить как неоптимальный так и попросту невалидный код - если оптимизатор не понимает, что ты "поломал" какие то регистры, то всё полетит к чертям. Код из нуль-поста, например, неправилен и перестанет работать просто на других настройках оптимизации. Поэтому надо прям изучать по хорошим руководствам.
Оптимизировать такие вещи в 22-23 году ассемблерными вставками занятие бессмысленное, а часто ещё и глупое, в случае компилятора Интел или даже Майкрософт вы просто утомитесь соревноваться с компилем в оптимизации. Вставки имеет смысл делать в случае каких-либо архитектурно зависимых вещей типа математических операций или векторов прерываний. Ну или если у вас чип в ошейнике для муравья, где каждый бит дорог, или шелл на двести байт, и то часто си сделает лучше.
gcc ещё плохо контролирует регистры, и может быть так, что код вообще не будет работать, у меня не работал, использовал инструкцию счётчика тактов для контроля быстродействия, VS справилась, а gcc нет, правда, я потом код подправил. Вот поэтому из х64 VS ассемблер удалили. Так что лучше асм код в длл, и использовать UASM или другие асмы с плюшками. И айтити синтаксис не перевариваю.
GCC вообще не анализирует никак текст инлайн-асма - для него это чёрный ящик. Поэтому надо через параметры-аннотации директивы инлайн-асма явно обозначать какие регистры трогаются, что является входными данными а что выходными. https://gamedev.ru/code/articles/gcc_inline_asm Например так: Код (Text): uint32_t a,b=5; asm( "movl %[arg_b],%%eax\n\t" "incl %%eax\n\t" :"=a"(a) :[arg_b]"m"(b) :"cc"); Здесь мы первой аннотацией поясняем GCC, что после выполнения нашего асма содержимое eax ассоциируется с переменной a, т.е. если вдруг сразу же после выполнения куска нашего асма нужна переменная a - её актуальное значение лежит в регистре eax и чтобы оно было актуальным в дальнейшем компилятору надо в памяти её значение обновить из eax. Он уже сделает это сам по своим соображениям оптимизации как ему лучше. Следующей аннотацией тут говорится, что мы рассчитываем, что нам на входе нужна переменная b и что мы возьмём её из памяти - а значит если компилятор до этого куска кода её закешировал в регистре - надо выложить обратно в память, чтобы не поломать логику нашего асма. Он это учтёт и сделает всё в лучшем виде. И последней аннотацией (clobber list) мы поясняем, что загрязняем регистр флагов - компилятор не может рассчитывать, что тот не изменился после выполнения этого асма. Прикол в том, что мы можем сказать, что на входе у нас в регистре eax должна лежать одна переменная (компилятор обязан это обеспечить), в регистре ebx - другая и так далее - и где эти переменные лежат на выходе. Поэтому компилятор сам на входе сделает нужные mov для заполнения исходных данных, а на выходе рассуёт их через mov в выходные переменные. Это позволяет GCC гибко оптимизировать, например, сишные inline-функции - полностью выкидывать перекладывания в память и обратно аргументов если функция чьё тело описано на асме заинлайнится прямо в месте вызова. Что MS VC сделать не может - он по природе встроенного в синтаксис асма вынужден будет повторять инструкции по загрузке переменных из памяти в регистры и обратно - просто сам принцип таков, что у него в теле функции должны быть инструкции по загрузке регистров из аргументов, а значит они будут заинлайнены тоже даже если совершенно не нужны. Поэтому в GCC можно сделать inline-функцию на асме над аргументами которая на выходе в лучшем случае превратится в одну инструкцию кода в бинарнике, а на MS VC это принципиально невозможно.