Pull to refresh

Comments 38

В эпоху, когда я занимался хакингом на 6502, наличие XOR было почти абсолютно точным указанием на то, что найдена часть кода, связанная с шифрованием, или какая-то подпрограмма обработки спрайтов. 

Во времена, когда я занимался программированием на ассемблере x86 это был рядовой приём, чтобы уменьшить размер кода, писали его, не задумываясь. Неожиданно встретить про него статью на «Хабре».

Угу. На 8080 был небольшой выбор: XOR A или SUB A (обе команды – 4 такта и 1 байт кода), а загрузка нуля командой MVI A, 0 – уже 7 тактов и два байта кода, так что о таком даже не задумываешься.

в ту эпоху это был, прямо скажем, best practice, я бы сказал!) любой, кто пишет на ассемблере только так и обнулял).. даже школьники (по своему примеру могу сказать)

Ичсх, на размер программы оно почти не влияет. Ну сколько раз вам необходимо обнулять EAX ?

1) Не так редко, как иногда кажется.

2) На размер влияет не очень сильно, конечно, но если у тебя на всё про всё, скажем, 4 Кбайта памяти, то обычно считаешь каждый байт.

3) Сейчас считают не байты в ОЗУ и тем более на дисках, а байты в строках кэшей и байты, которые процессор способен прочитать из кэша и обработать за один такт -- а там счёт по-прежнему идёт на единицы и десятки байтов.

4) А иногда, наоборот, для производительности выгодней раздувать программу. Скажем, если у тебя на ПК четыре команды вместились в 15 байт, причём первая из них начинается по адресу, кратному 16, то для запуска в работу пяти команд процессору потребуется два или три такта: в первом такте он выберет эти 16 байт, обнаружит, начиная с их начала, четыре полные команды и запустит их, а во втором такте он вынужден будет выбрать те же 16 байт (они ещё не закончились), но обнаружит там лишь одну команду или вообще её начало (в последнем байте этой группы) и запустит только её. А если она многобайтовая, ему придётся прочитать (в третьем такте) следующие 16 байт, чтобы эту команду запустить.

А вот если эти четыре команды искусственно раздуть (добавив, например, ничего не значащий префикс к одной из них) до 16 байтов, то процессор в первом такте выберет те же 16 байт и запустит эти четыре команды, но в следующем будет считывать уже следующие 16 байт -- в которых, если повезёт, тоже будет четыре команды, которые он сможет запустить. В результате получится 8 команд за два такта, а не 5 команд за два или три такта.

Ну, так просто напоминаю, что xor'ить можно двумя способами: 31 C0 или 33 C0... разные ассемблеры генерили разный код, и эти тройки и единицы хорошо выделялись на hex-дампе.

Таких подробностей я не помню. Почему два кода?

в отличие от других частичных записей в регистр, при записи в e-регистры наподобие eax архитектура без лишних затрат обнуляет старшие 32 бита. Поэтому xor eax, eax обнуляет все 64 бита.

Как я понимаю, при разработке 64-битной архитектуры уже было понимание, что частичные регистры (ah, al, ax) мешают внеочередному и спекулятивному исполнению (вызывают partial register stall), поэтому решили, что любая запись в e*x должна обнулять старшие 32 бита.

Я так делал еще, когда под zx spectrum на asm писал. Там это было и экономия памяти и оптимизация производительности. А с xor удобно было стирать спрайты и восстанавливать фон не перересовывая весь экран, что ускоряло анимацию и убирало моргание экрана.

извините не ЦПУ, а поидее парсер из кода в ассемблер, еквиваленты парсятся из дерева в ассемблер как я понимаю. В это можно погрузиться написав 2 стадии - парсер/лексер, далее высокоуровневый ассемблер(промежуточный или байткод ) да да я ошибся, но по итогу эта стадия такая в целом), и далее высокоуровневый в ассемблер

тоесть имея промежуточный код можно оставаться на этапе интерпретатора или идти дальше и компилировать бинарник

что интересно файл с байт-кодом это не бинарник всё еще

например есть промежуток push 5, зная какая платформа можно сгенерировать её инструкцию, для этого нужно знать инструкции платформы ну тоесть будет либо смещение либо push 5

Вы сами-то поняли что написали? Я вот нет.

да, потомучто не цпу генерирует инструкцию, а тот проход который зная платформу, зная инструкции, генерит это соответсвие на обнуление

ллвм имеет байткод = проомежуточный код, вы не генерируете без байткода код платформы

или вы по операции +(или 4*3+1-2) генерируете сразу ассемблер без промежутка?

ассемблер в ассемблер гнать проще

от байткода мы имеем адреса переходов и количество переменных(и текущее состояние стека) как минимум и удобно для отладки

тут даже не важно регистровая виртуальная машина или стековая, суть одна переносимость высокоуровнего языка в асемблер же

чтоб перенести код я считаю нужен асемблер высокоуровневый, его мы генерируем после лекс-парсинга, далее стоит выбор, или выполнить это на вм или перегнать в асемблер же, разве не так?

и как раз push 5 -> push 5 эквивалентно надо только перенести состояние стека на цпу как раз, а это уже не сложно если вм отрабатывает и отлаживаема намного удобнее чем сам ассемблер

мы не затрагиваем такую тему как генерация выходного кадра из компилятора gcc/clang ".S"

соотв. нужен синтаксис, дерево и байткод(вм), в асемблер

Есть однозначное соответствие между xor eax, eax и байтами 31 c0 для x86. Никаких зависящих от компилятора, проходов (заднего, переднего и т.д.), виртуальных машин и их байткодов вещей нет.

а как тогда программа из такого вида

Скрытый текст
int fib(int n) {
    if (n < 1) {
        return n;
    }
    // 
    int a = 0;
    int b = 1;
    int temp = 0;
    while (n > 1) {
        temp = a + b;
        a = b;
        b = temp;
        n--;
    }
    return b;
}

int main() {

    int result = 0;
    int counter = 0;
    int iterations = 100000; // 100 тысяч итераций


    while (counter < iterations) {
        result = fib(24); // Вычисляем fib(24) в каждой итерации

        counter++;
    }





    print(result);
    print(counter);
    return 0;
}

получает бинарник, окей допустим мы знаем адреса соотвествий, там на сколько помню еще ошибки выводит в консоль, и прочее

добавлю суть в том, что в статье явно приведён синтаксис

int main(){ <
  return 0; <
} <

тоесть какая-то программа соотнесла этот синтаксис с точкой входа прологом эпилогом и строкой xor

int возвращаемое значение из программы в ней return 0; и точка входа main у мейн еще аргументы могут быть, и она всё проверила помимо точки с запятой на конце

и за место этого синтаксиса отправила асемблер, вы пишите что у xor есть соответствие, но в сравнении со сборкой регистров, что сложнее, чем просто xor подставить, потомучто надо посчитать аргументы, возвращаемые значения, бывает сохранить состояния, причем нельзя же писать в 1 регистр, полюбому надо 2-3 регистра

тоесть на вход конечного автомата принимается выход из какого-то другого конечного автомата, тут как не крути не выходит всё просто

Я понял о чём Вы, но Вы, похоже, скипнули заголовок и суть поста - xor eax, eax, а не Си-в-Азм (передать ноль).

я написал самый первый коментарий о том, что процессор ничего не делает, а только следует конвееру команд, которые поступают из потока который может называться программа у которой есть точка входа, и ведёт это к той программе а не к процессору, процессор только инструкции имеет, и знает как их выполнять, а решение принимает не процессор, какую ставить команду

Процессор мягко говоря не только следует конвейеру команд. Да, если рассматривать исполнение только в рамках одного потока, то результат будет выглядеть так, будто бы это утверждение верно.

Тем не менее, с разбега можно назвать: аллокация/переименование регистров, спекулятивное исполнение, предсказание ветвлений, эвристики префетча памяти, кэширование чтения и буферизация записи. Многое из этого никак или почти никак не контролируется из кода напрямую и выполняется самим процессором, часто даже без спецификации. И это всё ещё не затрагивая протоколы когерентности, если мы начинаем мыслить в рамках нескольких потоков исполнения и/или ядер.

Я честно говоря не уверен, что верно распарсил направление вашей мысли и стараюсь угадать, что вы вообще могли иметь ввиду, но даже с учётом этого, в ваших утверждения точно достаточно много фактических ошибок. Возможно, они основаны на недостаточном понимании устройства реальных современных процессоров.

Не совсем однозначное, есть нюанс - я его упомянул в комментарии выше...

Извините, но это бред

Какой вы бестактный молодой человек;)

Напомнили олд-скульщикам про их возраст: этой фишке 20 лет в обед.

Препод в универе рассказал мне 20+ лет назад, что ему рассказывал его препод, что на новомодных тогда пнях такой фортель выполняется за ноль тактов. Побольше даже двадцати, получается)))

Исключающее ИЛИ для обнуления широко использовалось и, например, на Системе 360, а это середина 1960-х. Так что не 20 лет, не 20... :)

Ld de, 100
Push de 
Pop hl
Inc hl
Ld bc, 100
Ldir

Эх. Были времена

Да, фишка очень старая. Сейчас такие оптимизации скорее «по инерции». Сейчас простой экзешник с программой, отображающей пустое окно, в дефолтной конфигурации IDE, соберётся либо более чем в 1 мегабайт, либо в пятерку мелких файлов, которые надо таскать за собой, либо... Скучаю по тем временам, когда эти оптимизации действительно ценили, они имели вес

Ну, мегабайт. И что ? На современном диске - 1/1000000 обьема. В процентном соотношении - все равно что 200 байт 30 лет назад.

Но емкость дисков росла, чтобы это процентное отношение уменьшать, а не поддерживать постоянным...

Ну, когда то диски были 200 Мб. 1/1 000 000 == 200 байт. 200 байт exe для windows 95 - в студию, плз.

Процентное отношение уменьшается.

Сейчас -- отнюдь не "по инерции". Как в статье замечено, принудительное обнуление всего регистра облегчает жизнь многим (хотя и не всем) процессорам: благодаря этой операции логика, отвечающая за внеочередное выполнение команд, сразу знает, что обнулённый регистр не тянет зависимости от логически ранее расположенных команд в последующие, что нередко ускоряет выполнение.

Кроме того, практически у всех современных процессоров есть ограничения на выборку и декодирование команд: за один такт процессор может выбрать только 16 байтов, выровненных по естественной границе, и декодировать из их состава до четырёх команд одновременно. Соответственно, экономия на длине команды тоже важна: если у тебя не влезло четыре команды в эти 16 байт, процессор уже не сможет декодировать за такт именно четыре команды, что может создать "голод" для последующих стадий выполнения.

Какая разница, какой размер исполняемого файла в абсолютном выражении? Важно только то, какую долю от числа операций составляет операция обнуления регистра. И есть подозрение, что эта доля не сильно изменилась с тех пор.

А как тогда правильно выставить в ноль только младший дворд rax?

Не исключено, что только с помощью какого-нибудь AND RAX, RBX, где в RBX лежит константа FFFF'FFFF'0000'0000. Надо читать доку на AMD64, а мне лениво.

Чтобы не тратить драгоценные байты на mov rdx,0xFFFFFFFF00000000 (целых 10 байт), можно сделать например, вот так:

mov	edx,eax  ; 2 байта
xor	rax,rdx  ; 3 байта (можно `sub`)

Ещё вариант (чуть длиннее, но без доп. регистров):

shr rax,32  ; 4 байта
shl rax,32  ; 4 байта

Ну и до кучи вариант обнуления 31 младшего бита (не 32-х):

and rax,0x80000000  ; 6 байт (тут происходит знаковое расширение константы до 64 бит)

Чтобы не тратить драгоценные байты на mov rdx,0xFFFFFFFF00000000

Насколько помню, загрузить 64-разрядную константу можно только в RAX, в другие регистры нельзя.

Есть множество способов обнулись регистр, например:

xor eax,eax ; а также pxor xmm0,xmm0; xorps, xorpd, vpxor, etc...
sub eax,eax ; sbb, если cf=0
and eax,0
lea eax,[0]
push 0 / pop eax
salc ; al=0, если cf=0
cbw ; ah=0, если старший бит al=0; а также cwd, cdq, cqo
xchg eax,ebx ; ax=0, если bx=0 и наоборот; mov ax,bx аналогично
fldz ; fninit (не совсем обнуление, конечно, но как варик)
vzeroall ; vzeroupper
mov eax,eax ; старшая часть rax обнуляется
mov eax,0 ; внезапно

; странные способы (обфускация, например):
loop $ ; ecx=0; dec eax/jnz $-1 (для 32 битов); можно сделать rdtscp/inc ecx/loop $ (чтоб не гонять слишком долго)
mul ecx ; eax=edx=0, если ecx=0; аналогично fmul, fmulp, mulps, pmul...
mov ecx,-1 / div ecx ; eax=0, если edx=0; есть также divps и пр.
aad 0 ; ah=0
aam 1 ; ah=0
aam 0 ; al=0
shr ax,16 ; shr ax,cl, если cl = 16..31; можно shl
bzhi eax,eax,ecx ; если ecx=0
mov ecx,0FEh / rdmsr ; edx=0
mov eax,80000000h / cpuid ; ah=ebx=ecx=edx=0
movzx eax,al ; очищаем старшие 24 бита (56 в x64); есть ещё pmovzx
in ax,dx ; если правильно выбрать порт

; если значение ax заранее известно и очень подходит под ситуацию, можно inc eax, dec eax, not eax, lodsb, scasw, bswap и т.д.
; если знаем, что в памяти, то можно lds, les, mov eax,[ebx], pop ecx (например, на старте com-программы), xlatb, lodsw и т.д.
; по любому есть ещё 100500 способов

Когда-то, в былинные времена, были конкурсы - кто больше придумает способов обнулить регистр ax.

Все ораторы выше забыли про способ связанный с in

А еще, бонусом, очищаются флаги переноса и знака, и устанавливаются флаги нуля и четности.
Почему-то в статье об этом ни слова.

Sign up to leave a comment.

Articles