Учебник Нортона/Соухэ под Win64

Тема в разделе "WASM.BOOKS и WASM.BLOGS", создана пользователем ormoulu, 30 ноя 2022.

  1. ormoulu

    ormoulu Well-Known Member

    Публикаций:
    0
    Регистрация:
    24 янв 2011
    Сообщения:
    1.208
    Это я понимаю, потому и считаю что ссылаться в ресурсы форума смысла нет.
     
  2. GRAFik

    GRAFik Active Member

    Публикаций:
    0
    Регистрация:
    14 мар 2020
    Сообщения:
    352
    Thetrik нравится это.
  3. ormoulu

    ormoulu Well-Known Member

    Публикаций:
    0
    Регистрация:
    24 янв 2011
    Сообщения:
    1.208
    Арифметика процессора x64
    Зная кое-что о шестнадцатеричной арифметике отладчика Windbg и двоичной арифметике
    процессора x64, мы можем начать изучение того, как процессор выполняет свои математические
    операции. Он использует специальные команды, называемые инструкциями.

    Регистры как переменные
    Windbg, наш гид и интерпретатор, много знает о процессоре x64. Мы будем использовать его,
    чтобы исследовать внутренние процессы, происходящие в процессоре; и начнем с запроса к
    Windbg, чтобы тот напечатал, что он может сообщить о маленьких кусочках памяти, называемых
    регистрами, в которых могут храниться числа. Регистры похожи на переменные в языках высокого
    уровня, но они не совсем то же самое. В отличие от языков высокого уровня, процессор x64
    содержит ограниченное число регистров, и эти регистры не являются частью памяти вашего
    компьютера.

    Мы запустим Windbg уже знакомым способом ( Ctrl+E , открыть test64.exe) и попросим показать
    содержимое регистров процессора с помощью команды "r" ("Register"):
    Код (Text):
    1. 0:000> r
    2. rax=0000000000000000 rbx=00007ff9a9435a10 rcx=00007ff9a93ad564
    3. rdx=0000000000000000 rsi=00007ff9a9435900 rdi=0000000000000010
    4. rip=00007ff9a93e0950 rsp=000000c93ed4f070 rbp=0000000000000000
    5. r8=000000c93ed4f068 r9=0000000000000000 r10=0000000000000000
    6. r11=0000000000000246 r12=0000000000000040 r13=0000000000000000
    7. r14=000000c93ef8b000 r15=00000217c7a30000
    8. iopl=0 nv up ei pl zr na po nc
    9. cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b
    10. efl=00000246
    11. ntdll!LdrpDoDebuggerBreak+0x30:
    12. 00007ff9`a93e0950 cc int 3
    13.  
    (Вероятно, вы увидите немного другие числа; эти значения могут изменяться от запуска к запуску Windbg. Эти различия связаны с тем, что адреса для инструкций и данных программы в современных операционных системах выбираются относительно случайным образом, или рандомизируются. Позже мы это обсудим.)

    Сейчас Windbg выдал нам достаточно много информации. Обратим внимание на первые четыре регистра, rax , rbx , rcx и rdx . Это регистры общего назначения. Также регистрами общего назначения являются r8 , r9 , r10 , r11 , r12 , r13 , r14 , r15 - новые регистры, добавленные в архитектуре процессора x64. Регистры rsi , rdi , rip , rsp , rbp имеют специальное назначение (хотя rsi , rdi и rbp также могут использоваться и как регистры общего назначения). О связанных с этими регистрами инструкциях мы постепенно узнаем в дальнейшем.

    Число, показанное сразу вслед за именем регистра, является шестнадцатеричным. Ранее мы узнали, что четверное слово описывается шестнадцатью шестнадцатеричными цифрами. Как вы видите, каждый из регистров общего назначения является четверным словом и имеет длину 64 бита. Поэтому процесcopы архитектуры x64 называются 64-разрядными.

    Мы уже отмечали, что регистры похожи на переменные в языках высокого уровня. Это означает, что мы можем изменять их состояние, и мы будем это делать. Команда Windbg "r" не только отображает регистры. Если указать в команде имя регистра, то Windbg поймет, что мы хотим взглянуть на содержимое именно этого регистра:
    Код (Text):
    1. 0:000> r rbx
    2. rbx=00007ff9a9435a10
    Эта же команда "r" позволяет при желании изменить содержимое регистра, например как показано ниже:
    Код (Text):
    1. 0:000> r rax=3a7
    Давайте опять просмотрим содержимое регистров, чтобы убедиться в том, что в регистре rax теперь содержится ЗА7h:
    Код (Text):
    1. 0:000> r
    2. rax=00000000000003a7 rbx=00007ff9a9435a10 rcx=00007ff9a93ad564
    3. ...
    Так и есть. Итак, мы можем помещать шестнадцатеричное число в регистр с помощью команды "r", указывая имя регистра и вводя его новое значение после знака "равно", как мы только что сделали. В дальнейшем мы будем использовать эту команду для ввода чисел в регистры процессора.

    Вы наверно помните, что уже видели число ЗА7h , когда мы применили команду Windbg "?" для сложения 3А7h и 1EDh . Тогда Windbg делал работу за нас. Теперь же мы будем использовать Windbg больше как интерпретатор, чтобы работать непосредственно с процессором. Мы будем задавать инструкции на сложение чисел из двух регистров: сначала поместим число в регистры rax и rbx , затем дадим процессору инструкцию прибавить число, хранящееся в rbx , к числу, хранящемуся в rax , и поместить ответ обратно в rax . Мы знаем, что можем поместить в регистры rax и rbx нужные нам числа с помощью команды "r", но каким образом мы сообщим процессору о том, что их, то есть содержимое регистров rbx и rax , нужно сложить? Для этого мы введем некоторые числа в память компьютера.

    Память программы в Windows x64
    Для того, чтобы процессор мог прочитать и выполнить наши инструкции, они должны быть размещены в памяти в виде особой последовательности двоичных чисел - машинного кода. В данном случае машинный код будет представлен тремя двоичными числами (тремя байтами), с помощью которых мы сообщим процесcopy о том, что надо прибавить регистр rbx к rax . Затем для того чтобы мы могли увидеть результат, мы выполним эту инструкцию с помощью программы Windbg.

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

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

    Все байты виртуального адресного пространства процесса помечаются числами, начиная с 0h и выше. Помните, что регистры процессора x64 могут содержать максимальное число из шестнадцати шестнадцатиричных цифр, которое умещается в восемь байт? Это означает, что максимальный объем виртуальной памяти, который может адресовать программа, составляет 2^64, то есть примерно 18 квинтиллионов байт (примерно 18 эксабайт, или же ровно 16 эксбибайт ). На деле, далеко не любые из этих адресов доступны для программы.

    Для того, чтобы к каким-то из адресов виртуальной памяти процесс мог обратиться для чтения или записи данных, или же выполнения кода, эта память должна быть выделена операционной системой, то есть блоки физической памяти компьютера должны быть спроецированы на эти адреса виртуального адресного пространства процесса. Блоки памяти, которыми оперирует Windows называются страницами ("page") памяти и обычно имеют размер 1000h байт, или примерно 4 килобайт.

    upload_2023-4-16_11-19-6.png
    Проецирование операционной системой физической памяти в виртуальую память процесса.

    Все адреса, которые мы будем использовать в дальнейших примерах, принадлежат виртуальному адресному пространству нашего процесса. Благодаря операционной системе мы можем использовать эти 64-разрядные адреса не заботясь о том, в каких ячейках физической памяти компьютера расположены наши данные. Также в дальнейшем, говоря "память", в том числе например: "адрес в памяти", "страница памяти", "выделение памяти" и т.п., мы будем иметь ввиду именно виртуальную память и виртуальное адресное пространство процесса. Каждый адрес соответствует одному байту адресного пространства, и адреса расположены в возрастающем порядке, так что 00007ff7`308a1001h - байт следующий в памяти за 00007ff7`308a1000h .

    Сразу после создания процесса (то есть, запуска экземпляра программы) Windows выделяет в адресном пространстве этого процесса блоки виртуальной памяти, необходимые для работы программы, и копирует (с определенными изменениями) в эту память данные из исполняемого файла. Адрес, начиная с которого в виртуальной памяти располагается представление исполняемого файла, называется базовый адрес модуля ("module base addres"), в некоторых случаях этот адрес также может называться описателем модуля ("module handle").

    Но откуда программа должна в итоге начать работу? Для того, чтобы программа работала правильно, ее выполнение должно начаться с определенного места в машинном коде, называемого точкой входа ("entry point"). Исполняемые файлы Windows имеют специальный формат, в котором помимо данных и кода содержится различная служебная информация. Эта информация включает и адрес, с которого программа должна начать выполнение. Из-за того, что адреса страниц для копирования данных программы в память выбираются относительно случайным образом, этот адрес имеет не абсолютное значение, а относительное, то есть смещение относительно базового адреса модуля программы, загруженной в виртуальную память процесса. Такой относительный адрес сокращенно принято обозначать как RVA - "relative virtual address".

    Прибавив относительное смещение точки входа к базовому адресу модуля (то есть адресу виртуальной памяти, с которого начинается проекция исполняемого файла), операционная система получает виртуальный адрес точки входа нашей программы. По этому адресу должен находиться машинный код, то есть инструкции, которые начнет выполнять процессор, когда программа получит управление. К счастью для нас, Windbg способен произвести все эти сложные вычисления самостоятельно. Вычисленный адрес точки входа в программу отладчик сохраняет в переменной (называемой также "псевдорегистром") с именем $exentry . Вы можете узнать этот адрес с помощью уже знакомой нам команды "?":
    Код (Text):
    1. 0:000> ? $exentry
    2. Evaluate expression: 140696330440704 = 00007ff6`6acc1000
    Если вы только открыли исполняемый файл в отладчике Windbg, вы не увидите машинный код, начинающийся с точки входа программы, потому что Windbg останавливает процесс до того, как программа получила управление. Оказаться в том месте нашего машинного кода, с которого должно начаться исполнение, поможет команда "g" ("Go"). Через пробел от "g" указывается адрес, на котором отладчик должен остановить выполнение программы, это может быть и переменная $exentry . Будьте осторожны с этой командой! Если вы забудете указать адрес, или такой адрес не встретится при выполнении, вы потеряете контроль над программой.

    Код (Text):
    1. 0:000> g $exentry
    2. test64+0x1000:
    3. 00007ff6`6acc1000 90 nop
    Если все прошло успешно, отладчик отобразил адрес точки входа и машинный код, который там находится. Убедитесь еще раз (при помощи команды ? $exentry ), что этот адрес действительно совпадает с адресом начала программы. Сейчас по этому адресу расположена инструкция nop , которая ничего не делает (либо какая-то другая инструкция если вы запустили в отладчике не test64.exe, а другой исполняемый файл). Заменим ее на инструкцию, которая делает что-то полезное - складывает числа.

    Команда Windbg для просмотра и изменения памяти называется "e" ("Enter"). Используйте эту команду для ввода трех байт инструкции, как показано ниже. Когда вы ввели все три числа в память, нажмите еще раз клавишу "Ввод", чтобы завершить работу команды "e":

    Код (Text):
    1. 0:000> e $exentry
    2. 00007ff6`6acc1000 90 48
    3. 00007ff6`6acc1001 90 03
    4. 00007ff6`6acc1002 90 C3
    5. 00007ff6`6acc1003 90
    Числа 48h , 03h и C3h теперь расположены по адресам 7ff6`6acc1000h , 7ff6`6acc1001 и 7ff6`6acc1002 . Вероятно адреса, которые вы увидите, будут другими, но это различие не будет влиять на нашу программу. Как и в приведенном примере, Windbg выведет двухзначные числа после значений адресов. Эти числа ( 90h в нашем примере) старые значения байтов машинного кода в точке входа программы, тo есть эти числа - код, который был в исполняемом файле. Мы только что заменили эти байты инструкцией сложения регистров.

    upload_2023-4-16_11-24-42.png
    Инструкция сложения, начиначинающася с точки входа программы, по смещению 1000h от базового адреса модуля.
     
    DrochNaNoch, GRAFik и Thetrik нравится это.
  4. ormoulu

    ormoulu Well-Known Member

    Публикаций:
    0
    Регистрация:
    24 янв 2011
    Сообщения:
    1.208
    Сложение, метод процессора x64

    Наша инструкция сложения в нужном месте виртуальной памяти. Используйте команду Windbg "r", чтобы установить значения регистров rax и rbx :

    Код (Text):
    1. 0:000> r rax=3A7
    2. 0:000> r rbx=92A
    Теперь регистры должны выглядеть так (используйте всё ту же команду "r"):

    Код (Text):
    1. 0:000> r
    2. rax=00000000000003a7 rbx=000000000000092a rcx=000000c93ef8b000
    3. rdx=00007ff66acc1000 rsi=0000000000000000 rdi=0000000000000000
    4. rip=00007ff66acc1000 rsp=000000c93ed4fa38 rbp=0000000000000000
    5. r8=000000c93ef8b000 r9=00007ff66acc1000 r10=0000000000000000
    6. r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
    7. r14=0000000000000000 r15=0000000000000000
    8. iopl=0 nv up ei pl zr na po nc
    9. cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b
    10. efl=00000246
    11. test64+0x1000:
    12. 00007ff6`6acc1000 4803c3 add rax,rbx
    13.  
    Инструкция add помещена в память именно там, где мы хотели ее разместить. Это видно из нижней строки сообщения. Шестнадцатеричное число 00007ff6`6acc1000 это адрес первого байта нашей инструкции. Далее мы видим три байта, означающие add : 4803c3 . Байт, равный 48h , расположен по адресу 7ff6`6acc1000h , a C3h по адресу 7ff6`6acc1002h . Наконец, поскольку мы записали нашу инструкцию на машинном языке - в виде чисел, не имеющих значения для нас, но интерпретируемых процессором как инструкция - сообщение add rax,rbx подтверждает, что мы записали инструкцию верно.

    После того, как инструкция размещена в памяти, необходимо сообщить процессору о том, где она расположена. Процесcop x64 находит адрес следующей инструкции в специальном регистре, rip , который вы уже видели в списке регистров. Этот регистр принято называть счетчиком команд или же указателем инструкций ("instruction pointer"). Но если вы посмотрите на предыдущие листинги, то увидите, что значение регистра rip уже равно $exentry , куда мы поместили нашу инструкцию. Вы можете еще раз убедиться в этом при помощи команды "?":

    Код (Text):
    1. 0:000> ? $exentry
    2. Evaluate expression: 140696330440704 = 00007ff6`6acc1000
    3. 0:000> ? rip
    4. Evaluate expression: 140696330440704 = 00007ff6`6acc1000
    Регистр rip стал равным $exentry после выполнения команды "g $exentry". Обратите внимание, что эта команда будет выполнять все машинные инструкции начиная с текущей и остановится на нужном адресе только если инструкция по этому адресу действительно должна быть выполнена! Поэтому командой "g" следует пользоваться весьма аккуратно, однако "g $exentry" это удобный способ остановиться на точке входа только что запущенной программы.

    Теперь, когда наша инструкция на месте, а регистры установлены в нужные значения, мы попросим Windbg выполнить эту единственную инструкцию. Мы применим команду Windbg "t" ("trace"), которая выполняет одну инструкцию за шаг и затем показывает следующую. После каждого шага регистр rip будет указывать на адрес следующей инструкции, в нашем случае он будет указывать на 7ff6`6acc1003h . Мы не помещали никакой новой инструкции в 7ff6`6acc1003h , поэтому после выполнения команды "t" мы увидим инструкцию, которая изначально была в этом месте программы. Давайте с помощью команды "t" попросим Windbg выполнить одну инструкцию, а затем командой "r" проверим содержимое регистров:

    Код (Text):
    1. 0:000> t
    2. test64+0x1003:
    3. 00007ff6`6acc1003 90 nop
    4. 0:000> r
    5. rax=0000000000000cd1 rbx=000000000000092a rcx=000000c93ef8b000
    6. rdx=00007ff66acc1000 rsi=0000000000000000 rdi=0000000000000000
    7. rip=00007ff66acc1003 rsp=000000c93ed4fa38 rbp=0000000000000000
    8. r8=000000c93ef8b000 r9=00007ff66acc1000 r10=0000000000000000
    9. r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
    10. r14=0000000000000000 r15=0000000000000000
    11. iopl=0 nv up ei pl nz ac po nc
    12. cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b
    13. efl=00000216
    14. test64+0x1003:
    15. 00007ff6`6acc1003 90 nop
    Вот и все. Регистр rax теперь содержит число CD1h , которое является суммой ЗА7h и 92Ah . A регистр rip указывает адрес 7ff6`6acc1003h , так что в нижней строке мы видим инструкцию, расположенную в памяти по адресу 7ff6`6acc1003h , а не по адресу 7ff6`6acc1000h .

    Код (Text):
    1.  Перед выполнением инструкции сложения:
    2. rax: 00000000000003a7
    3. rbx: 000000000000092a
    4. rip: 00007ff66acc1000 ==> add rax,rbx
    5. nop
    6.  
    Код (Text):
    1.  После выполнения инструкции сложения:
    2. rax: 0000000000000cd1
    3. rbx: 000000000000092a
    4. add rax,rbx
    5. rip: 00007ff66acc1003 ==> nop
    Как отмечалось ранее, указатель инструкции rip всегда указывает на следующую инструкцию, которую нужно выполнить процессору. Если мы опять напечатаем "t", то выполнится следующая инструкция. Но сейчас нет смысла делать этого, ведь следующая инструкция nop ничего не делает. Вместо этого, почему бы не выполнить введенную инструкцию еще раз, то есть сложить 92Ah и CD1h и сохранить новый ответ в rax ? Что нам надо сделать для того, чтобы объяснить процессору где найти следующую инструкцию, и чтобы этой следующей инструкцией оказалась та же add rax,rbx , расположенная по адресу 7ff6`6acc1000h ? Можем ли мы изменить значение регистра rip на 7ff6`6acc1000h ? Давайте попробуем. Но чтобы не беспокоиться о численном значении адреса, которое довольно велико и может изменяться между запусками программы, мы используем уже знакомую нам переменную $exentry . Присвойте rip значение $exentry командой "r", и посмотрите распечатку регистров:
    Код (Text):
    1. 0:000> r rip=$exentry
    2. 0:000> r
    3. rax=0000000000000cd1 rbx=000000000000092a rcx=000000c93ef8b000
    4. rdx=00007ff66acc1000 rsi=0000000000000000 rdi=0000000000000000
    5. rip=00007ff66acc1000 rsp=000000c93ed4fa38 rbp=0000000000000000
    6. r8=000000c93ef8b000 r9=00007ff66acc1000 r10=0000000000000000
    7. r11=0000000000000000 r12=0000000000000000 r13=0000000000000000
    8. r14=0000000000000000 r15=0000000000000000
    9. iopl=0 nv up ei pl nz ac po nc
    10. cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b
    11. efl=00000216
    12. test64+0x1000:
    13. 00007ff6`6acc1000 4803c3 add rax,rbx
    14.  
    Попробуйте еще раз ввести команду "t" и посмотрите, содержит ли регистр rax число 15FBh . Действительно содержит.

    Как видите, пepeд тем, как использовать команду "t", вам необходимо проверить регистр rip и соответствующую его значению инструкцию, располагаемую в нижней части листинга, показываемого командой "r". Таким образом, вы будете уверены, что процессор выполняет нужную инструкцию.

    А сейчас, установите регистр rip в $exentry , убедитесь, что в регистрах содержится rax = 15FBh , rbx = 92Ah , и давайте попробуем произвести вычитание.

    Вычитание, метод процессора x64

    Мы собираемся написать инструкцию для вычитания rbx из rax , так что после двух вычитаний в регистре rax появится результат ЗА7h . Тогда мы вернемся к той точке, с которой начали. Кроме того, вы увидите, каким образом можно немного сэкономить усилия при вводе нескольких байтов в память.

    Когда мы вводили три байта инструкции add командой "e", то печатали численное значение каждого байта в следующей строке, по одной строке для каждого адреса. Однако мы можем ввести все байты в одну строку, отделяя значение каждого следующего байта пробелом. После окончания ввода нажмите клавишу "Enter". Попробуйте этот метод на нашей инструкции вычитания:

    Код (Text):
    1. e $exentry 48 2b c3
    Листинг регистров (помните о необходимости установки регистра rip в $exentry ) должен теперь показать инструкцию sub rax,rbx , которая вычитает содержимое регистра rbx из регистра rax и размещает результат в rax . Порядок записи rax и rbx может показаться противоположным, но эта инструкция похожа на выражение А = A - B , написанное на языке высокого уровня, с той разницей что процессор, в отличие от языков программирования, всегда помещает ответ в первую переменную (в первый регистр).

    Выполните эту инструкцию с помощью команды "t". rax должен содержать CD1h . Измените rip так, чтобы он указывал на эту инструкцию (командой r rip=$exentry ), и выполните ее опять (не забывайте сначала проверять инструкцию внизу листинга регистров). rax теперь должен содержать 03A7h .

    Отрицательные числа в процессоре x64

    Ранее мы узнали, как процессор x64 использует форму двоичного дополнения для отрицательных чисел. Сейчас мы поработаем непосредственно с инструкцией sub , чтобы проводить вычисления с отрицательными числами. Давайте дадим процессору небольшой тест, чтобы посмотреть, получим ли мы FFFFFFFFFFFFFFFFh в качестве -1. Мы вычтем единицу из нуля, и, если мы были правы, то в результате вычитания в регистре rax должно оказаться FFFFFFFFFFFFFFFFh (-1). Установите значение rax равным нулю и rbx равным единице, затем запустите инструкцию по адресу равному $exentry . Мы получили то, что ожидали: rax = FFFFFFFFFFFFFFFFh .

    Байты, слова и двойные слова в процессоре x64

    До этого вся наша арифметика совершалась над четверными словами, то есть шестнадцатью шестнадцатеричными цифрами. Знает ли процессор x64, как выполнять математические операции над байтами? Да, знает.

    Процессор способен оперировать как целым 64-разрядным регистром общего назначения, так и его "частями" меньшей разрядности. Это связано с тем, что "предки" нашего процессора имели разрядность 8 (один байт), затем 16 (два байта или слово) и наконец 32 бита (четыре байта или двойное слово). Как вы возможно помните, первый из 32х-разрядных регистров общего назначения назывался eax . Архитектура x64 позволяет обращаться к "младшей" (меньшей разрядности) 32хразрядной части регистра rax используя то же имя (и те же машинные коды), что и в IA-32. Соответственно, младшая 16-разрядная часть eax называется ax , и в свою очередь разделяется на старший байт ah и младший байт al .

    upload_2023-4-16_11-51-19.png

    Точно так же, к примеру, регистр rdx можно разделить на edx , dx , dh , dl . Процессор x64 способен оперировать этими частями регистров по отдельности. Проверим байтовую арифметику на инструкции add . Введите два байта 00h и С4h , начиная с адреса $exentry :

    Код (Text):
    1. 0:000> e $exentry 00 C4
    Установите регистр rip в $exentry , и внизу листинга регистров вы увидите инструкцию add ah,al , которая суммирует два младших байта регистра rax и поместит результат в старший из них, ah . Затем загрузите в младшую 16-разрядную часть регистра rax число 102h . Вы можете сделать это командой "r", обратившись к этой младшей части по имени ax .

    Код (Text):
    1. 0:000> r ax=102
    2. 0:000> r rax
    3. rax=ffffffffffff0102
    Таким образом вы поместите 01h в байт ah и 02h в al . Выполните команду "t", и вы увидите, что регистр rax теперь содержит FFFFFFFFFFFF0302h . Результат сложения 01h + 02h будет 03h , и именно это значение находится в байте ah .

    Так же как и байтами, процессор способен оперировать частями регистров из двух байт - словами ax , bx , cx и так далее.

    Операции с 32-разрядными регистрами

    Инструкции процессора, изменяющие часть регистра размером в один или два байта, никак не влияют на остальную ("старшую") часть регистра. В случае четырехбайтовых регистров (двойных слов) eax , ebx , ecx , edx , esi , edi и прочих, все обстоит несколько сложнее. При записи значений в 32-битовую "младшую" часть регистра, в которую входят биты под номерами от 0 до 31, старшая часть 64-битового регистра, то есть биты 32-63, всегда обнуляется. Проверим это при помощи инструкции add eax, ebx , код которой 01 D8 (не забудьте установить rip в $exentry ):

    Код (Text):
    1. 0:000> e $exentry 01 D8
    2. 0:000> r rip=$exentry
    3. 0:000> r rax
    4. rax=ffffffffffff0302
    5. 0:000> t
    6. test64+0x1002:
    7. 00007ff6`6acc1002 c3 ret
    8. 0:000> r rax
    9. rax=00000000ffff0303
    Как видите, старшая часть регистра rax стала равной нулю. Эту неочевидную особенность желательно помнить при арифметических и других операциях с частями регистров.

    Умножение и деление, метод процессора x64

    Мы видели, как процессор x64 складывает и вычитает два числа. Теперь мы увидим, что он может также умножать и делить. Инструкция умножения называется mul , а машинный код для умножения rax на rbx : 48 F7 E3 . Мы введем его в память, но сначала несколько слов об инструкции mul .

    Где инструкция mul сохраняет ответ? В регистре rax ? Не совсем; здесь надо быть аккуратными. Как вы можете догадаться, умножение двух 64-битных чисел может дать ответ разрядностью в 128 бит, так что инструкция mul сохраняет результат в двух регистрах, rdx и rax . Старшие 64 бита помещаются в регистре rdx , а младшие - в rax . Мы можем также записать эту комбинацию регистров как rdx:rax .

    Давайте вернемся к Windbg. Введите инструкцию умножения 48 F7 E3 по адресу $exentry , как вы это делали для инструкций сложения и вычитания, и установите rax = 7С4Вh и rbx = 01000000`00000000h . Установив регистр rip в $exentry , вы увидите инструкцию в листинге регистров как mul rax,rbx . В действительности эту инструкцию можно записать как mul rbx , и в дальнейшем вам, возможно, придется так делать. Это связано с тем, что при умножении 64- разрядных регистров, процессор x64 всегда умножает peгистр, который вы указываете в инструкции, на регистр rax , и сохраняет ответ в паре регистров rdx:rax .

    Перед тем, как мы запустим эту инструкцию умножения, давайте произведем умножение вручную. Как мы можем подсчитать 01000000`00000000h * 7C4Bh ? Цифры "0", обозначающие порядок, имеют в шестнадцатеричной системей такой же эффект, как и в десятичной, так что умножение на 01000000`00000000h просто добавит четырнадцать нулей справа от шестнадцатеричного числа. Таким образом, 01000000`00000000h * 7C4Bh = 7С`4В000000`00000000h . Этот результат слишком длинен для того, чтобы поместиться в одном четверном слове.

    Используйте Windbg для трассировки инструкции. Вы увидите, что rdx содержит 7Ch, а rax содержит 4b000000`00000000h . Другими словами, микропроцессор x64 возвращает результат инструкции умножения четверных слов в паре регистров rdx:rax . Так как результат умножения двух четверных слов не может быть длиннее двух четверных слов, но часто бывает длиннее одного четверного слова (как мы только что видели), инструкция умножения четверных слов всегда возвращает ответ в паре регистров rdx:rax .

    Код (Text):
    1.  Перед выполнением инструкции умножения:
    2. rdx: 00007ff66acc1000
    3. rax: 0000000000007C4B
    4. rbx: 0100000000000000
    5. rip: 00007ff66acc1000 ==> mul rax,rbx
    6. nop
    7.  
    Код (Text):
    1.  После выполнения инструкции умножения:
    2. rdx: 000000000000007c
    3. rax: 4B00000000000000
    4. rbx: 0100000000000000
    5. mul rax,rbx
    6. rip: 00007ff66acc1003 ==> nop
    7.  
    А как насчет деления? Когда мы делим числа, процессор x64 сохраняет как результат, так и остаток от деления. Посмотрим на выполнение деления в x64. Поместим инструкцию 48 F7 F3 по адресу $exentry . Как и инструкция mul , div использует пару регистров rdx:rax , не сообщая об этом, так что все, что мы видим - это div rax,rbx . Значения регистров: rdx уже содержит 7Ch; rbx по-прежнему должен содержать 01000000`00000000h . Регистр rax содержит значение 4B000000`00000000h ; вспомним, что младший байт регистра rax носит имя al и установим его значение в 12h , так что значение rax станет 4B000000`00000012h :

    Код (Text):
    1. 0:000> r al=12
    2. 0:000> r rax
    3. rax=4b00000000000012
    4.  
    Подсчитаем результат вручную: 7С`4В000000`00000012h / 01000000`00000000h = 7C4Bh с остатком 12h . После выполнения инструкции деления по адресу $exentry мы получим для rax = 7C4Bh результат нашего деления и для rdx = 12h , остаток. Мы найдем этому остатку очень хорошее применение, когда будем писать программу перевода десятичных чисел в шестнадцатеричные.

    Код (Text):
    1. Перед выполнением инструкции деления:
    2.  
    3. rdx: 000000000000007c
    4. rax: 4B00000000000012
    5. rbx: 0100000000000000
    6.  
    7. rip: 00007ff66acc1000 ==> div rax,rbx
    8. nop
    Код (Text):
    1. После выполнения инструкции деления:
    2.  
    3. rdx: 0000000000000012
    4. rax: 0000000000007C4B
    5. rbx: 0100000000000000
    6.  
    7. div rax,rbx
    8. rip: 00007ff66acc1003 ==> nop
    Как и в случае с операциями сложения и вычитания, процессор x64 способен умножать и делить не только четверные слова, но и числа меньшей разрядности: байты, слова и двойные слова. И подобно тому, как для умножения и деления четверных слов всегда используется пара rdx:rax , процессор использует пары регистров для чисел меньшей разрядности. Это пара edx:eax для двойных слов (помните, что в результате старшие части rax и rdx станут равны нулю), dx:ax для 16-битных слов и ah:al для однобайтовых чисел.

    Итог

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

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

    Мы также узнали о том, как сообщить Windbg o необходимости выполнить (протрассировать) одну инструкцию. Конечно, по мере того как программы будут расти, их трассировка будет становиться как более полезной, так и более утомительной процедурой. Позже мы еще раз вспомним команду "g", с помощью которой можно выполнить несколько инструкций одной командой Windbg.
     
    Последнее редактирование: 16 апр 2023
    DrochNaNoch, GRAFik, Mikl___ и ещё 1-му нравится это.