Как зациклиться на сегменте кода размером 64 КБ в программе, которая пришивает себя к собственному хвосту, до бесконечности?

avatar
Sep Roland
8 августа 2021 в 21:30
89
1
3

Если последовательное выполнение инструкций проходит через смещение 65535, то 8086 выберет следующий байт инструкции со смещения 0 в том же сегменте кода.

Следующая программа .COM использует этот факт и постоянно сшивает весь свой код (всего 32 байта) в свой хвост, оборачиваясь в сегмент кода размером 64 КБ. Вы могли бы назвать это бинарным квайн.

    ORG 256            ; .COM programs start with CS=DS=ES=SS

Begin:
    mov  ax, cs        ; 2 Providing an exterior stack
    add  ax, 4096      ; 3
    mov  ss, ax        ; 2
    mov  sp, 256       ; 3
    cli                ; 1
    call Next          ; 3 This gets encoded with a relative offset
Next:
    pop  bx            ; 1  -> BX is current address of Next
    sub  bx, 14        ; 3  -> BX is current address of Begin
More:
    mov  al, [bx]      ; 2
    mov  [bx+32], al   ; 3
    inc  bx
    test bx, 31        ; 4
    jnz  More          ; 2
    nop                ; 1
    nop                ; 1
    nop                ; 1

В интересах инструкций call и pop программа установит небольшой стек вне сегмента кода. Я не думаю, что cli действительно необходим, потому что у нас есть стек.
Как только мы вычислили адрес текущего начала нашей 32-байтной программы, мы копируем его на 32 байта выше в памяти. Вся арифметика указателя BX будет выполняться циклически.
Затем мы проваливаемся во вновь написанном коде.

Если последовательное выполнение инструкций проходит по смещению 65535, то 80386 вызовет исключение 13.

Предполагая, что я включил необходимые настройки для обработчика исключений, будет ли достаточно просто выполнить дальний переход к началу этого сегмента кода (где вновь написанный код находится в ожидании)? И будет ли такое решение действовать на процессорах после 80386?


Связано: Можно ли сделать программу на ассемблере, которая будет писать себя вечно?

Источник
prl
8 августа 2021 в 21:40
1

Это может работать только в том случае, если граница инструкции попадает на границу сегмента. Является ли размер вашей программы степенью двойки, чтобы она равномерно делилась на сегменты?

Sep Roland
8 августа 2021 в 21:42
1

@prl Код имеет 32 байта и начинается со смещения 256. Оба являются подходящими степенями двойки. Если бы я расширил это, это стало бы 64 байта.

ecm
8 августа 2021 в 22:05
0

«И останется ли такое решение действительным на процессорах после 80386?» Я верю в это.

Peter Cordes
8 августа 2021 в 22:56
1

Не могли бы вы закончить jmp $+2 вместо истинного nop, чтобы явно обрезать IP-адрес на 386, если это необходимо? Я думаю, вы могли бы смотреть на это как на настоящий прыжок, вместо того, чтобы позволять обертыванию происходить естественным образом. (Это также позволило бы избежать предварительной выборки устаревших инструкций на всех реальных ЦП, даже если они имеют больший буфер предварительной выборки, чем 6 байтов на 8086, что достаточно мало, чтобы не быть проблемой.)

Sep Roland
8 августа 2021 в 23:18
0

@PeterCordes Это хороший способ избежать повторного выполнения последовательного выполнения, но даже с jmp $+2 процессору придется выполнить перенос 64 КБ в регистре IP. Так что, возможно, это одно и то же... Кроме того, вопрос, который меня вдохновил, как бы настаивал на провале в новом коде. переходное решение, которое не может и не должно давать сбоев, заключается в замене nop инструкцией jmp bx. Зацикливание в регистре BX всегда безопасно.

Peter Cordes
8 августа 2021 в 23:23
0

@SepRoland: 16-битные переходы размера операнда do усекают IP на реальном оборудовании без сбоев. Вот почему o16 jmp rel16 нельзя использовать в обычном 32-битном коде (ассемблер GAS не использует 2-байтовое кодирование относительного смещения JMP (только 1-байтовый или 4-байтовый)). Это не ошибка, даже если позволить выполнению идти самостоятельно на 386, как указывает ваша цитата. (т. е. если приращения счетчика программ неявно используют EIP, поэтому они нарушат ограничение на сегмент, если перенесут ненулевые биты в верхнюю половину EIP)

Peter Cordes
9 августа 2021 в 09:19
2

Хорошо, ответ Маргарет подтверждает, что нам не нужно явное усечение IP; он будет сворачиваться сам по себе без jmp даже на 386, если у вас нет висящей многобайтовой инструкции. Кстати, supercat предложил увеличить еще один указатель в блокноте с IP вместо того, чтобы копировать его каждый раз, например. ADD SP,SI / PUSH AX / PUSH BX / PUSH CX / NOP для хранения байтов инструкций. Это кратно 6, но может быть адаптировано к 8 байтам.

Ответы (1)

avatar
Margaret Bloom
9 августа 2021 в 08:59
3

В 16-разрядном режиме (реальном или защищенном) регистр IP будет без ошибок переносить 64 КБ, при условии, что ни одна инструкция не пересекает границу 64 КБ (например, двухбайтовая инструкция, помещенная в 0xffff).

Инструкция пересечения вызовет ошибку на 80386+, не знаю, что произойдет на предыдущих моделях (прочитать следующий байт в линейном адресном пространстве? прочитать следующий байт из 0?).

Обратите внимание, что это работает, потому что лимит сегмента совпадает с лимитом регистра IP.
В 16-битном защищенном режиме вы можете установить лимит сегмента менее 64 КБ, в этом случае выполнение завершится ошибкой при достижении конца.
Короче говоря (и фигурально), ЦП проверяет, что все необходимые ему байты находятся в пределах ограничения сегмента, а затем увеличивает счетчик программ без обнаружения переполнения.

Таким образом, ваша программа должна работать.


Вероятно, его можно с некоторой натяжкой назвать quine, потому что он читает свой собственный машинный код, а это обман (точно так же, как чтение файла исходного кода для языков высокого уровня).

Я не проверял это, но самым минимальным примером программы, "как бы копирующей" себя, может быть:

 ;Setup (assuming ES=CS)
 mov al, 0abh       ;This encodes stosb
 mov di, _next      ;Where to start writing the instruction stream

 stosb              ;Let's roll

_next: 

Это также не квайн, потому что реплицируется только stosb.
Создать квайн сложно, хранилища должны быть инструкциями, кодировка которых меньше размера хранимых данных, иначе у нас всегда будет больше байтов для записи, чем записано.

supercat
9 августа 2021 в 19:11
2

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

Joshua
11 января 2022 в 21:43
0

На Pentium I это не сработало. Ошибка с IP=10000h. Я попробовал это на физическом Pentium I. У кого-нибудь есть 8086 или 80286, чтобы попробовать? Я подозреваю, что это не будет работать на 386.