07-assembly (1238882), страница 2
Текст из файла (страница 2)
zip-архив)•Вам необходимо исследовать логику работы но на этот раз не взломатьизменением кода, а вычислить с какими параметрами запустить, чтобыдоступ был выдан•Это задание можно взять на дом, оно потребует анализа ABI для функцииmainИнлайн-ассемблер в сишных функциях•Зная конвенции вызова можно написать программу частично на ассемблереint main () {// mov DWORD PTR [esp], 5// call _fact// mov DWORD PTR [esp+4], eax// mov DWORD PTR [esp], OFFSET FLAT:LC0// call _printf// mov eax, 0}•Для этого мы просто используем расширение языка:•asm ("movDWORD PTR [esp], 5" : out : in : clobber); // GCC•__asm movDWORD PTR [esp], 5;// MSVSИнлайн-ассемблер в сишных функциях•Зная конвенции вызова можно написать программу частично на ассемблереint main () {asm("mov DWORD PTR [esp], 5");// call _fact// mov DWORD PTR [esp+4], eax// mov DWORD PTR [esp], OFFSET FLAT:LC0// call _printf// mov eax, 0}•Проблема: до компиляции мы не можем быть уверены добавить ликомпилятор лидирующее подчёркивание•Решение: синтаксис с явными констрейнтамиИнлайн-ассемблер в сишных функциях•Зная конвенции вызова можно написать программу частично на ассемблереint main () {asm("mov DWORD PTR [esp], 5");asm("call %0"::"P0"(fact)); // instr : in : out : clobberasm("mov DWORD PTR [esp+4], eax");// mov DWORD PTR [esp], OFFSET FLAT:LC0// call _printf// mov eax, 0}•Проблема: между строчками компилятор волен что-нибудь вставить•Чтобы решить эту проблему используется конкатенация литераловпрепроцессоромИнлайн-ассемблер в сишных функциях•Зная конвенции вызова можно написать программу частично на ассемблереint main () {asm("mov DWORD PTR [esp], 5\n""call %0\n""mov DWORD PTR [esp+4], eax\n"::"P0"(fact));// mov DWORD PTR [esp], OFFSET FLAT:LC0// call _printf// mov eax, 0}•Проблема: до компиляции мы не можем быть уверены куда компиляторназначит строковый литералИнлайн-ассемблер в сишных функциях•Мы явно выносим строку и используем аргумент в памяти и явное leaconst char *name = "Hello, world!\n"; // okint main () {asm("mov DWORD PTR [esp], 5\n""call %P0\n""mov DWORD PTR [esp+4], eax\n""lea eax, %1\n""mov DWORD PTR [esp], eax\n""call %P2\n"::"i"(fact),"m"(name[0]),"i"(printf));}•Всё равно ничего не работает.
Дело в том, что компилятор распределяетname в eax, но вызов функции внутри тихо портит (клобберит) именно eax.Инлайн-ассемблер в сишных функциях•Используем клоббер-лист и в итоге всё срастаетсяconst char *name = "Hello, world!\n"; // okint main () {asm("mov DWORD PTR [esp], 5\n""call %P0\n""mov DWORD PTR [esp+4], eax\n""lea eax, %1\n""mov DWORD PTR [esp], eax\n""call %P2\n"::"i"(fact),"m"(name[0]),"i"(printf):"eax");}•Как видно из проведённого исследования хорошая вставка на ассемблеренетривиальна. Подробнее: https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.htmlОбсуждение•Ассемблерные вставки делают код более оптимальным?•Увы, это не всегда так.
Для достаточно сложной программы вручнуюобогнать оптимизирующий компилятор – амбициозная задача•Хуже того: при эволюции кода ассемблерные вставки навсегда застревают впрошлом и их приходится переписывать, а не перекомпилировать•Плюс подумайте о поддержке для разных платформ. Ассемблер сильноотличается даже x86 от x86-64Оптимизации компилятора•Ключи оптимизации контролируют какой именно ассемблерный кодполучится у компилятора. Обычные уровни оптимизации•-O0 (по умолчанию) – здесь компилятор старается сделать ассемблернаиболее точно воспроизводящий написанное программистом•-O1 (базовые оптимизации) – применяются простые оптимизации•-O2 (продвинутые оптимизации) – применяются все основные оптимизации•-O3 (агрессивные оптимизации) – применяются дополнительные инебезопасные оптимизации, включая векторизацию•-Os (оптимизировать размер) – применяются оптимизации размера вместобыстродействияКак правильно писать на ассемблере•Утверждения ниже аннотированы вероятностью того, что они верны•Не надо программировать на ассемблере (p = 0.8)•Никогда не надо программировать на ассемблере (p = 0.16)•Возможно вам всё-таки надо что-то написать на ассемблере (p = 0.04)••Напишите это на C, скомпилируйте и посмотрите на ассемблерный код.
Послеэтого скопируйте и при необходимости модифицируйте его (p = 0.032)•Если этого нельзя написать на C, вам не повезло (p = 0.008)Ну что же, кажется настало время немного попрограммировать наассемблере...Problem IA: подсмотреть регистры•Распечатайте значения регистров eax, ebx, ecx, edx в начале функции main•Обратите особое внимание, чтобы распечатать именно те значения, которыебыли в момент входа в main. Проверьте себя в gdb> problem_iaeax = 0x1ebx = 0x1d2bb4ecx = 0x759d6bbaedx = 0x1d1c10•Есть соблазн вызвать функцию printf тоже из ассемблера, но это довольносложно.•Постарайтесь написать минимальную ассемблерную вставкуProblem LG – шпионская функция•У вас есть бинарный файл и вам надо его модифицировать таким образом,чтобы вставить в начало функции interesting вызов функции spyme, причёмтело этой функции вы сами должны вставить в бинарник•Сейчас ограничьтесь распечаткой инкрементирующегося счётчика•После модификации вывод должен быть такой:> problem_lg1 2 3 4 5 6 7 8•Понятно, что так можно не только счётчик распечатывать, но и кредиткиворовать.
Но мы же честные люди (кроме того см. legal disclaimer к Problem CM)Builtins•Многие полезные вещи не имеют простого выражения в языке C•Пример: подсчёт всех установленных битов в числе. На C сложно придуматьчто-то лучшее, чем циклunsigned mask = 1;int cnt = 0;for (;;) {if ((n & mask) == mask)cnt += 1;if (mask == (1u << 31))break;mask = mask << 1;}Builtins•Многие полезные вещи не имеют простого выражения в языке C•Пример: подсчёт всех установленных битов в числе. На C сложно придуматьчто-то лучшее, чем цикл•Но весь этот цикл можно легко заменить на один интринсикint cnt = __builtin_popcount(n);•Идея в том, что компилятор явно умеет некоторые вещи лучше нас, но он незнает что программист имел в виду.
Даже если цикл действительно делаетподсчёт установленных бит, об этом сложно догадаться•Билтин это способ сказать компилятору явно чего от него нужно. И тогда,например в ARM, он может просто использовать инструкцию cnt, котораяделает это на аппаратном уровнеСравнение ассемблера ARMПросто циклИспользование __builtin_popcountuxtw x0, w0fmov d0, x0cnt v0.8b, v0.8baddv b0, v0.8bumov w0, v0.b[0]movmovmovmovw3,w2,w0,w1,w03201.L4:bics wzr, w1, w3lsl w1, w1, 1cinc w0, w0, eqsubs w2, w2, #1bne .L4Обсуждение•А что компилятор сделает, если инструкции просто нет?•Например в x86 нет простой возможности подсчитать установленные битыСравнение ассемблера x86Просто циклИспользование __builtin_popcountmov eax, edishr eaxand eax, 1431655765sub edi, eaxmov eax, ediand eax, 858993459shr edi, 2and edi, 858993459add edi, eaxmov eax, edishr eax, 4add eax, ediand eax, 252645135imul eax, eax, 16843009shr eax, 24movxormovjmpecx, 32eax, eaxedx, 1.L4.L6:add edx, edx.L4:mov esi, edxand esi, edicmp esi, edxsete silmovzx esi, siladd eax, esisub ecx, 1jne .L6Ассемблер x86, march=nehalem*Просто циклИспользование __builtin_popcountmovxormovjmppopcnt eax, ediecx, 32eax, eaxedx, 1.L4.L6:add edx, edx.L4:mov esi, edxand esi, edicmp esi, edxsete silmovzx esi, siladd eax, esisub ecx, 1jne .L6*2008 год, NehalemСопроцессор и плавающие числа•Обработка плавающих чисел на заре x86 происходила в отдельном FPU•Сейчас это давно не так, но ассемблер остался с тех времёнdoublefoo (double f1,double f2) {double ftmp;f1 = 1.0 - f1;f1 = f2 * f1;ftmp = f1 / 3.0;f2 = f1 + ftmp;return f2;}fldfldfsubrfldfmulpfxchfdivfaddpQWORD PTRst(0)DWORD PTRQWORD PTRst(1), stst(1)DWORD PTRst(1), st[esp+4]LC0[esp+12]LC1st[0]st[1]st[2]st[3]st[4]st[5]st[6]st[7]Расширения регистров: MMX и SSE•Сопроцессор с отдельным стеком этоисторическая редкость, конечно•Для эффективной работы с плавающей точкой,расширение MMX добавило в архитектуру восемь64-битных регистров MM0-MM7 отображающихсяна старый стек сопроцессора, т.е.
MM0 это 64битная часть st[0]•На самом деле, чистый MMX это такая древность,которая уже тоже не встречается•Расширения SSE, SSE2, SSE3 добавили регистрыxmm0-xmm15, отдельные от старого стека,размером в 128 битmm0mm1mm2mm3mm4mm5mm6mm7st[0:7]xmm0xmm1xmm2xmm3xmm4xmm5...Новый ассемблер с xmm регистрами•Исходникdoublefoo (double f1,double f2) {double ftmp;f1 = 1.0 - f1;f1 = f2 * f1;ftmp = f1 / 3.0;f2 = f1 + ftmp;return f2;}••Былоfldfldfsubrfldfmulpfxchfdivfaddp•QWORD PTRst(0)DWORD PTRQWORD PTRst(1), stst(1)DWORD PTRst(1), st[esp+4]LC0[esp+12]Сталоmovsdsubsddivsdmulsdaddsdxmm2,xmm2,xmm0,xmm2,xmm0,QWORD PTR LC0xmm0QWORD PTR LC1xmm1xmm2LC1Важен также тот факт, что больше не надо думать о стеке сопроцессораProblem RA – инверсный корень снова•Реальный замер инверсного корня из Problem RI даёт на современныхмашинах неутешительные результаты•Ваша задача: заменить всю функцию ассемблерной вставкой сиспользованием специальной инструкции PFRSQRT•Обратите внимание: эта сравнительно новая инструкция работает только сxmm регистрамиВекторизация на SSE регистрах•Наличие широких команд приводит к тому, что циклы выгодно векторизовать.LBB0_1:movdqa xmm0, xmmword ptr [rdx + 4*rax]movdqa xmm1, xmmword ptr [rdx + 4*rax + 16]movdqa xmm2, xmmword ptr [rdx + 4*rax + 32]movdqa xmm3, xmmword ptr [rdx + 4*rax + 48]padddxmm0, xmmword ptr [r8 + 4*rax]padddxmm1, xmmword ptr [r8 + 4*rax + 16]padddxmm2, xmmword ptr [r8 + 4*rax + 32]padddxmm3, xmmword ptr [r8 + 4*rax + 48]movdqa xmmword ptr [rcx + 4*rax], xmm0movdqa xmmword ptr [rcx + 4*rax + 16], xmm1movdqa xmmword ptr [rcx + 4*rax + 32], xmm2movdqa xmmword ptr [rcx + 4*rax + 48], xmm3addrax, 16cmprax, 256jne.LBB0_1int a[256], b[256], c[256];void foo () {int i;// этот цикл может быть векторизован// и копировать массивы// блоками по 64 байта// используя xmm регистрыfor (i=0; i<256; i++){a[i] = b[i] + c[i];}}Расширения регистров: AVX••Расширения AVX добавили ymm и zmmрегистры, размером 256 и 512 байтсоответственно и инструкции для работы снимиzmm0ymm1,ymm2,ymm3,ymm8,ymm4,ymm5,ymm6,ymm9,ymmwordymmwordymmwordymmwordymmwordymmwordymmwordymmwordptrptrptrptrptrptrptrptr[rip[rip[rip[rip[rip[rip[rip[rip++++++++b]b+32]b+64]b+96]c]c+32]c+64]c+96]xmm0xmm1Фрагмент векторизации того же кодаvmovdquvmovdquvmovdquvmovdquvmovdquvmovdquvmovdquvmovdquymm0ymm1zmm1zmm2zmm3zmm4...ymm2ymm3ymm4...xmm2xmm3xmm4...Домашняя работа HWV – векторизация•Исследуйте применение векторизации в умножении вектора на матрицуfor (i = 0; i < XSZ; ++i) {int acc = 0;for (j = 0; j < YSZ; ++j)acc += vect[j] * matr[j][i];result[i] = acc;}•Может ли этот код быть эффективно векторизован?•Исследуйте его быстродействие с разными уровнями оптимизациями иразмерами векторов и матрицЛитература•С11 ISO/IEC – "Information technology – Programming languages – C", 2011•& Brian W.
Kernighan, Dennis Ritchie – The C programming language, 1988• Intel Software Developer Manual: intel-sdm• Peter van der Linden – Expert C Programming: Deep C Secrets, 1994• Юров В., Хорошенко С. – Assembler: учебный курс, 1999• С.В. Зубков – Assembler. Язык неограниченных возможностей, 2007• Chris Lomont – Introduction to Intel® Advanced Vector Extensions, 2011• Capabilities of Intel® AVX-512 in Intel® Xeon® Scalable Processors, 2017.