Skip to content

Latest commit

 

History

History
972 lines (683 loc) · 55.2 KB

lection.md

File metadata and controls

972 lines (683 loc) · 55.2 KB

Конспект по курсу ассемблера.

Осторожно этот конспект был написан одним человеком, и может содержать ошибки и не человекочитаемые фразы.

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

Используемые источники:

Введение

Ассемблер -- это не язык программирования. Это мнемоника команд процессора. Из этого следует что, сколько существует ISA, только и различных ассемблеров.

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

Сначала мы рассмотрим x86(32 bit), потом x64 (x86-64 bit)

История процессоров

  • i8086 (1978) -- 16bit
  • i80186 (1982)
  • i80286 (1982)
  • i80386 (1985-1994) -- 32bit
  • i80486 (1989-1994)
  • Pentuim [Pro] (1993 [1995])
  • Pentuim MMX (1997)
  • Pentuim II (порезанный Pro) (1997)
  • Pentuim III (1999)
  • Pentuim M (2003) -- M=mobile
  • Core 2 (2006) -- 64bit

NB) x86 - reg-mem2, cisc

Полезные ссылки

Обзор регистров

eax = [32bit] = [16bit]:ax (ax -- младшие 16 бит от eax, только для e*x регистров) ax = ah:al (только для *x)

Иногда не совсем понятно, почему существуют некоторые ограничения и почему некоторые команды предпочтительнее других. Это чаще всего происходит из-за неодинакового размера кода команды, который ограничен 15 байтами, поэтому пытаются экономить.

Регистры общего назначения (General Proposal)

  • eax - accumulator
  • ecx - counter
  • edx - data
  • ebx - base
  • esi - source index
  • edi - destination index
  • esp - stack pointer
  • ebp - base pointer

Регистры букв важен только в метках

Замечание: лучше всего использовать регистры той битности, что и режим процессора.

Также есть eip (instruction pointer) - в коде мы его не можем использовать. (почти)

EFLAGS - регистр, которые содержит различные флаги.

  • ZF (zero flag) -- нулевой результат вычисления.
  • CF (carry flag) -- знаковое переполнение результата.
  • SF (sign flag) -- знак результата.
  • OF (overflow flag) -- знак беззнакового переполнения.
  • DF (direction flag) -- нужен для особых команд.

Обзор команд

пересылка дынных

mov

mov eax, ebx
mov al, ah
mov ax, [ebx]
mov [eax + ebx * 4], ebx

Что можно писать в скобках:

  • [base_register32 + index_register32 * scale_factor + offset32]
    • base_register32 = general proposal 32-bit register
    • index_register32 = general proposal 32-bit without esp
    • scale_factor = {1, 2, 4, 8}
  • [base_register16 + index_register16 + offset16]
    • base_register16 = {bx, bp}
    • index_register16 = {si, di}

Замечание: Некоторые компиляторы, например yasm, позволяют в скобках писать 5*eax. Они это просто сконвертируют, к верхней схеме, если смогут.

Также иногда необходимо указать размер пересылаемых данных [byte, word, dword]:

mov dword [eax], 5
mov [eax], dword 5

другие

  • xchg -- "swap". Между регистрами почти бесплатный, а по памяти -- дорогая, так как является блокирующей.
  • bswap -- меняет порядок байт (endian)
  • movsx/movzx -- меньшее в большее, первая заполняет знаковым битом, а вторая 0.
  • lea (load effective address) -- вычисляет адрес и записывает его в первые операнд.
  • push/pop -- команды работы со стеком: положить и снять.
  • pushad/pusha -- выгрузить все общие регистры на стек (8 штук); первая для 32битных, вторая -- для 16битных регистров. Порядок, как указано выше.
  • popad/popaa -- снять все регистры со стека. Но esp не запишется.

переходы

  • jmp -- прыгает на указанный адрес, можно передавать регистр. (просто меняет eip)
  • call -- вызывает функцию. То же самое, что jmp ...+ push eip.
  • ret/ret const -- возврат из функции. То же самое, что pop eip. Вторая команда дополнительно снимает const байт со стека, т. е. то же что pop eip + add esp, const.
  • j__ -- условные переход на метку, в зависимости от состояние флагов. Нельзя передавать регистр. Работает предсказание переходов.
    • c/z/s -- если соответствующий флаг установлен: je label
    • [n] + a/b + [e] -- беззнаковое сравнение(на самом деле просто комбинация флагов после вычитания): jnae label
    • [n] + g/l + [e] -- знаковое сравнение
  • cmov__ -- условная загрузка. Суффикс как выше. Не предсказывается.

арифметика

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

  • add/adc -- сложение/сложение + флаг переноса. (carry flag)
  • sub/sbb -- вычитание/вычитание - флаг переноса.
  • mul rx -- беззнаковое умножение.
    • mul r8 => ah:al = ah * r8
    • mul r16 => dx:ax = ax * r16
    • mul r32 => edx:eax = eax * r32
  • imul -- знаковое умножение
    • imul reg => как выше
    • imul dst, src => dst *= src
    • imul dst, src, const => dst = src * const
  • div/idiv arg32 => edx:eax = (edx:eax / arg32):(edx:eax % arg32) Если результат не влезет в eax будет ошибка переполнения.
  • cwd/cdq -- делает регистровую пару edx:eax/dx:ax, заполняя знаковым битом.
  • inc/dec -- инкремент/декремент. Не меняет флаги.
  • neg - арифметическое отрицание.
  • and/or/xor/not/andn -- побитовые логические операции
    • xor eax, eax -- обнуление eax, это стандартный способ обнуления. Занимает меньше кода, но меняет флаги.
  • cmp/test -- соответствует sub/and, но меняет только флаги. В сочетании с условными переходами позволяет делать условные конструкции.
    • cmp/test + j__ -- часто это одна реальная команда, поэтому не нужно их лишний раз разделять.
    • test eax, eax -- стандартный способ проверки на 0.
  • shr/shl -- логические сдвиги (заполнение нулям), sar/sal -- арифметические сдвиги (заполнение знаковым битом при правом сдвиге, левый сдвиг равен логическому.)
    • последний потерянный бит сохраняется в CF
    • при сдвиге на больше чем 32 будет использоваться только 5 бит
  • shld/shrd -- сдвиги двойной точности. (Можно делить пару)
    • shld reg1 reg2, const -- сдвиг, но потерянные биты беруться из reg2
  • ror/rol -- циклический сдвиг
  • rcl -- циклический сдвиг следующей штуки: (..←[31..0]←[cf]←..)
  • rcr -- циклические сдвиг следующей штуки: (..→[31..0]→[cf]→..)

другое

  • int{x} -- вызвать прерывание, x = 3 -- вывалиться в отладчик.
  • nop -- ничего не делает
  • ud2 -- команда, которая гарантированно отсутствует.

Best practice

модуль числа

  test  eax, eax
  jns   L1
  neg   eax
L1:

% но лучше так (нет условных переходов):
  cdq
  xor   eax, edx
  sub   eax, edx

Деление 8

  cdq
  shr   edx, 29
  add   eax, edx
  sar   eax, 3

условные переходы

if(eax == 5 && ebx < 3) {
  X
} else {
  Y
}
  cmp   eax, 5
  jne   LE
  cmp   ebx, 3
  jnb   LE
  X
  jmp   LX
LE:
  Y
LX:

Циклы

do-while

do ... while(eax != 0);

Некоторые конструкции языков C/C++ существуют именно в таком виде, потому что так их можно эффективно реализовать.

L1:
  ...
  test  eax, eax
  jnz   L1

while-do

while(eax != 0) 
    ...
  jz    L2
L1:
  ...
  cmp   eax, 0
  jnz   L1
L2:

switch

switch (eax) {
  case 1:
    X
    break;
  case 3:
  case 4: Y
  case 6: Z
}
    cmp     eax, 6
    ja      LE
    jmp     dword [eax*4 + table]
L1:
    X
    jmp     LE
L2:
    Y
L3:
    Z
LE:
...
    section  rdata
    table  dd LE, L1, LE, L2, L2, LE, L3

Умножение на 10

    lea     eax, [eax*4 + eax]
    add     eax, eax

Деление через умножение

TODO

секции памяти

section .text
  • text -- код.
  • data -- данные.
  • rdata -- данные только для чтения.
  • bss -- неиницализированные данные. Почти не занимаю место в исполняемом файле.

Эти секции описывают свойства процесса. Не путать с сегментами! Они про адресацию.

Calling convention:

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

cdecl stdcall pascal fastcall
Куда класть аргументы? stack stack stack reg\stack
Порядок загрузки аргументов? c, b, a c, b, a a, b, c ?
Кто снимает аргументы? caller calling calling ?
Где возвращается заначение? edx:eax/eax/ax ⬅️ ⬅️ ⬅️
Сохраняемые регистры ebx, ebp, esi, edi ⬅️ ⬅️

Замечание: Сохранять все регистры, также как и не сохранять ничего -- дорого.
Пояснение: Пусть мы сохраняем все регистры: тогда в начале тела функции нужно сохранить все регистры, которые мы хотим использовать. Если мы не будет вообще сохранять регистры, то после вызова внешней функции нельзя пользоваться значениями в регистрах, поэтому опять надо все сохранять.

Как возвращается большие объекты?

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

thiscall

Это нужно для методов классов. Тут тоже пытаются класть this в нулевой аргумент. И поэтому порядок this и *result, если оба необходимо передавать в аргументах, является implementation behavior.

Примеры

  • printf("%s", p) // cdecl
    push  p
    push  fstr
    call  printf
    add   esp, 8
    ...
    section  .rdata
fstr: "%s", 0
  • int f(int a, int b) {return a*2 + b;} // cdecl
; stack = [ret][1st arg][2nd arg]...
    mov   eax, [esp + 4]
    add   eax, eax
    add   eax, [eax + 8]
    ret

cdecl vs stdcall

В stdcall не возможно написать printf, так как это vararg-функция. Такие функции требуют:

  • стек должен чистить caller, так как функция не может знать сколько аргументов ей передали.

Best practice

  • Конвенция вызова должна совпадать с конвенцией фукнции. Но это неважно для локальных фукнций, которые никто не будет вызвать, кроме вас.

  • если мы хотим сделать рефакторинг кода на Ассемблере, например, помнять логику программы и делаем push в середине кода, то все адреса стека, к которым обращаются ниже нужно изменить, так как стек сдвинулся. Поэтому удобно писать адрес обращения к аргументам функции из двух часте: mov eax, [esp + 4 + 4], первая цифра -- номер аргумента, вторая -- сколько мы пушнули.

  • Почитать Агнер Calling convention. TODO: добавить ссылку.

FPU/x87

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

У сопроцессора есть свои регистры: 8 регистров, которые собраны в зацикленный стек.

  • st[0..8]
    • 80 бит (extended presion)
    • Поддерживает разные типы:
      • dword(одинарная точность)
      • qword(двойная точность)
      • tbyte(extended точность)
      • BCD80(что-то с десятичной арифметикой)

Обзор команд

Загрузка

  • fld (from float)
  • fild (from integer)
  • fbld (bcd80)
  • fldpi/fld1/fldz (load pi/1/0)

Выгрузка

  • fst (first store)
  • fstp (fist store and pop)

При загрузке и pop стек крутиться и номера регистров меняются. Но при этом мы не можем затирать значения регистров, которые не освободили. Если это не соблюсти будет NaN.

Арифметические

  • fadd/fsub/fmul/fdiv - общие арифметические операции
  • fprem -- ищет остаток вычитанием, но не более 64 раз, после просто возвращает результат.
  • fabs, fchs, fsqrt, fsin, fcos, fsincos (ищет оба и кладет на стек.)
  • fcom -- сравнение и запись во флаги сопроцессора, поэтому нельзя делать бранчи с этой командой
  • fcomi -- сравнение + запись в CF, ZF. OF не записывается! jne - не работает

Другие

  • finit - помечает все регистры как доступные и устанавливает дефолтное значение.
  • fchx st1 (swap st1, st0)
  • [command] + ..r = [command with reverse order of argument]
  • [command] + ..p = [command with pop before load result]
  • ffree -- освободить регистр.
  • fincstp/fdecstp -- покрутить стек.
  • fisttp -- выгружает значение из регистра и выкидывает его со стек. При этом значение конвертится к целому числу отбрасыванием дробной части.

Calling convention

  • Вызванная функция должна зачистить стек командой finit перед использованием x87.
  • Если возвращаемое значение функции имеет вещественный тип, то оно кладется на вершину стека (st0). Это верно для любой конвенции 32битного мира.

Best practice

  • про конвертацию между целыми и вещественными числами: (касается не только x87)
    • Долго конвертировать uint в float, так как надо сделать проверки переполнения.
    • Долго конвертировать float в int, так как не совпадают стандартные округления: в C к 0, в asm к ближащему четному (default). Поэтому нужно переключить режим окргуления, а это долго.
    • Лучше не использовать ассемблерные вставки, так как это не переносимо: из-за компиляторазависимиого синтаксиса, а также msvc вообще не поддерживает их под 64bit. Поэтому лучше писать на ассемблере отдельные функции в отдельных файлах.

Пример: Как вызвыть функцию на asm, из кода C/C++:

// declaration
// C
float __cdecl arctan(float x, int n);
// C++ 
extern "C" // for name mangling
float __cdecl arctan(float x, int n);

SIMD-команды

SIMD - single instruction, multiple data. Это идея когда мы можем одной командой обрабатывать сразу несколько ячеек данных.

История расширений Intel для SIMD и не только. TODO

  • MMX (Pentium MMX, 1997) : mm0..7 - 64 бита мантисы регистров FPU, можно использовать для int. И обрабатывать сразу два int одной командой
  • SSE (Pentium III, 1999) : xmm0..7 - 128 битые регистрые, которые можно использовать как 4 float. Плюс, что это никак не связано с FPU, про который пытаются забыть.
  • SSE2 (Pentium 4) : можно использовать xmm для int.
  • SSE3 (later Pentium 4) : доп. команды.
  • SSE4 (early Core 2, 2006) : доп. горизонтальные команды.
  • SSE4.1 (later Core 2, 2008) : ...
  • SSE4.2 (2008) : ...
  • SSE4a (...) : ...
  • AVX (2012) : ymm - 256 битные регистры, но только для float
  • AVX2 (2013) : можно использовать ymm для int
  • AVX512 (2015) : zmm - 512 битные регистры
  • FMA3, FMA4 (2010) : fused multiply-add, инструкции типа a*b + c для x\ymm. Фича: округление происходит только один раз.
  • AES_NI (2008) : Advanced Encryption Standard New Instruction
  • BMI(2013) : bit manipulation instruction, добавились команды для некоторых логических выражений типа: ~x & x, -x & x, x & (x - 1).

MMX

Так как эти регистры накладываются на регистры x87, их нельзя использовать одновременно. Во время работы с mmx, все st помечаются как недоступные, и в конце нужно вызвать команду emms, которая пометит все st0..8, как доступные к использованию.

Замечание: Эта команда сильно похожа на finit, но последнаяя также устанавливает дефолтное значение.

Обзор Комнад (TODO Annotation)

  • movss - (SSE) load scalar single precision.

  • movap{s, d} - (SSE) load aligned packed single/double precision. Каждый из двух операндов может быть или памятью, или регистром. Память должна быть выровнена до 16 байт.

  • movup{s, d} - (SSE) load unaligned packed single/double precision. Тоже что выше, но память может быть не выровнена.

  • punpckl{bw, dq, qdq, wd} - unpack low data. {bw} - собирает word из byte, используя младшую часть регистов. Схема

  • punpckm{bw, dq, qdq, wd} - unpack high data. {bw} - собирает word из byte, используя старшую часть регистров. Схема

  • packss{wd, dw} - pack with signed saturation. Saturation - число конвертиться в число меньшей битности, так чтобы значение было ближе всего к исходному. Схема

  • packus{wd, dw} - pack with unsigned saturation.

  • pmulhw - multiply packed signed integer and store high result.

  • pmullw - multiply packed signed integer and store low result.

  • pmaddwd - multiply and add packed integer. Схема

  • pcmpeq{b, w, d} - Compare packed data for equal.

  • pcmpgt{b, w, d} - Compare packed signed Integer for greater than

  • pand/por/pxor/pandn - bitwise and/or/xor/and not

  • psrl{w, d, q} - Shift packed data right logical Схема

  • psra{w, d, q} - Shift packed data right arithmetic Схема

  • psll{w, d, q} - Shift packed data left logical. Схема

  • paddus{b, w} - add packed usigned intgers with unsigned saturation.

  • psubus{b, w} - sub packed usigned intgers with unsigned saturation.

  • palignr dst, src, imm8 - конкатенирует dst и src, потом сдавигает в право imm8 байт и записывает младщую часть в dst.

  • pshufw dst src imm8 - Shuffle the words in src based on the enocoding in imm8 and store the result in imm1. Схема

  • emms - Очищает все x87 регистры

  • movq/movd -- загрузить целочисленный qword/dword

Выравнивание

Как сделать выравнивание данных в asm?

  • на стеке
	section .data
	align 16
	const1: 12 
  • в куче
    • alloc + ручное выравнивание
    • align_alloc(Linux)/virtual_alloc(Windows)

Выравнивание стека

Перед любым вызовом функции в 32 битах жилательно выравнивать стек до 16, а в 64 битности - это обязательно.

Выделить n байт с выраниванием 16

	push   ebp
	mov    ebp, esp
	sub    esp, n
	and    esp, -16
	...
	mov    esp, ebp
	pop    ebp

История про разгон кода

TODO

Режимы работы процессора (16 и 32bit)

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

Режим работы меняет коды команд.

  • real 16
  • protected 16/32/v86
  • long mod 64/compatibility32/compatibility16

Биос начинает работать в real mode и потом постепенно переходит в нужный.

Real mode

Изначально в этом режиме работали 16 битные процессоры.

Адресация

В i80186 можно адресовать 1 Мб оперативки, то есть $2^{20}$

Физический адрес = сегмент * 16 + смещение(виртуальный адрес)

Сегментный регистры: cs(code), ds(data), ss(stack), es(extra). Они тоже состоят из 16 бит и могут хратить любой адрес.

Сегмент можно указывать явно,

mov al, [ebx] ; по-умолчанию

mov al, ds:[ebx]

ds: ; seg ds
mov al, [ebx]

а может выводиться по-умолчанию:

  • команды, которые всегда работают стеком(pop, push) используют ss
  • для адреса команд аналогично cs
  • для памяти мы смотрим регист, который используется как база:
    • bp, ebp, esp => ss (sp нет, потому, что его не бывает в квадратным скобках.)
    • Остальные к ds.
  • В некоторых командах есть свои умолчания.

Ограничение на размер сигмента Существует ограничение на размер сигмента 64Kb. Но его можно обойти написав сегмент ясно ds:[eax]. Важно также заметить, то в real mode можно использовать 32битные регистры для адресов начиная с i80386. Замечание: В "C" есть поэтому поводу оговорка, что объект не может занимать больше места чем максимальный размер смещения. (< 64kb)

Замечание:

mov al, [ebx + ebp] ; implementation undefined behavior

mov al, [ebx + ebp*1] ; ok, => ds
; хороший компилятор (not nasm) позволяет таким образом подавить использование ebp как базы

i80286

Может адресовать 24-битные адреса.

Добавились новые сегментные регистры: fs, gs с неопределенным использованием.

A20 line: в i80186 при обращении за границы 1 Мб происходило переполнение. То есть всегда использовались только последние 20 бит. А в i80286 можно адресовать большую память, поэтому переполения не происходило и можно было адресовать $2^{20} + 2^{16} - 2^{4}$ в real mode. Для обратной совместимости можно влючить переполнение. Про это Bios Line 20 disable

Проблемы Real Mode

  • Нельзя управлять памятью.
    • Невозможно безопастно делить ее между процессами.
    • Отделять код системы от кода пользователя.

Protected mode 16

Это режим появился в 286.

Теперь сегментный регистр содержит не базу сегмента, а селектор, который состоит из 3 частей.

[0..1] <- RPL
[2] <- G/L
[3..15] <- index
  • index - индекс дескриптора сегмента в глобальной таблице.

  • g/l - global/local description table, но локальные не используется.

  • rpl - права

    • 0 - макс. права
    • 3 - мин. права

Дескриптор сегмента состоит из 8 байт и содержит все необходимую информацию о сегменте:

  • Базу
  • Лимит
  • Флаги
  • Уровень привелегий.

Адрес таблицы дескрипторов храниться в регистре gdtr (global description table register).

Замечание: Для ускорения обращения у сегментного регистра есть теневая часть, в которую загружается дескриптор. Это имеет некоторые последствия.

Права

Теперь у нас есть 4 уровня прав. 0 - уровень ОС. 3 - уровень пользователя.

Это позволяет:

  • Разграничить пользовательский код и код ОС.
  • Ограничить доступ пользователя к данным.

CPL - current privilege level - уровень прав в регистре cs. Он пределяет права кода. В этом отличие пользовательского кода, от кода OC.

RPL - request privilege level - уровень прав в остальных сегментных регистрах, который определяет права для запроса на использование своего дескриптора.

DPL - descriptor privilege level - уровень прав в дескрипторе сегмента.

  • Если это сегмент кода, то это уровень привелегий это кода.
  • Если это сегмент данных, то уровень привелегий определяет, кто может обращаться к этим данным.

Замечание: Зачем нужен RPL, если есть DPL? Когда мы передаем управление более привелегированному коду и просим его, например, изменить данные сегмента, на который у нас прав нет. Тогда вызваемая функция может записать в RPL селектора, записать наш CPL. Так как при обращении к данным сверяется с DPL не только CPL, но и RPL, данные будут защищены.

Существуют инструкции в защищенном режиме можно использовать только при нулевом CPL.

Процесс может понизить свои права. Но как повысить?
Мы можем вызвать прерывание, у которого выше уровень привилегий. И он может вернуть нам права.

Адресация

Защита достикается повышением абстракции. Теперь адреса бывают:

  • логические(logical) - пара <сегмент>:<смещение>
  • линейные(linear) - промежуточный уровень(нужен для страничной адресации) = <сегмент> + <смещение>
  • физические(physical) - адрес, который подается в шину. <физческий адрес> = <линейный> (пока нет страничной адресации)

В 286 база сегмента составляла 24 бита, поэтому максимальная доступная память - $2^{24}$ = 16 Mb. При этом длина сегмента менялась от 1 до $2^{16}$ байт.

Переходы:

  • Ближние jump, call -- в приделах одного сегмента памяти.
  • Дальние: far_jump, far_call -- меняет также сегмент кода. jmp segment:offset

Flat-модель памяти.

Это идея обращения процесса с памятью. Она заключается в том, что память для процесса - это единое и не делимое пространство.

32bit защищенный режим

Это режим появился в 386. В нем увеличилась битность линейного адреса до 32 бит и появилась возможность делать сегменты с лимитом 4Gb. Это открыло доступ к Flat-module работы с памятью.

Размер дескриптора при этом не поменялся, так как в нем заранее присутствовали 2 неиспользуемых байта.

Также в дескрипторе есть флаг, который для сегмента кода значит, какой битности записаны команды: 16 или 32. (D-bit)
Подробнее о флагах на вики

Замечание: Теперь лимит не может принимать любые значения, так как он занимает всего 20 бит и увеличивается за счет изменения гранулярности с 1 байта на 4Кб.

Unreal mode

Существует незадокументированная фича, что при переходе в real mode теневая часть не меняется. Это дает доступ к сегментам большого размера в реальном режиме. Это называют нереальном режимом. (unreal mode)

Так как в real mode можно использовать 32 битные регистры, то можно получить доступ к 4Gb. Это фича называется Operand Size Override Prefix и включается отдельно(может добавляться автоматически компилятором ассемблера). И важно отключить вентиль A20.

TODO: Проверить: Видимо лимит на размер сегмента в реальном моде даже на i80186 хранился в скрытой части сегмента(видимо тогда она уже сущетсвовала.). Но я не нашел явно упоминания.

возможно лимит не надо было хранить, так как он всегда один и тот тоже.

v86

Режим совместимости с 8086. Адресация как в реальном режиме, но с кольцами прав.

SMM

System Management Mode - режим отладки. wiki

Страничная адресация

Это новый способ адресации, который был введен с 386.

Замечание: Почему на некоторых Win32 нельзя выделить больше 4 ГБ?
Это лишь лицензионное ограничение. На самом деле благодаря страничной адресации мы можем выделять довольно большое адресное пространство для программы. Так как система сама строит адресное пространство.

AWE - другой хак, который позволяет выделять процессу больше чем 4ГБ без страничной адресации.

Замечание: AWE(Address Windowing Extension) - суть заключается в окне в адресном пространстве программы, которое мы можем отображать в разные части пространства физических адрессов.

Замечание: Страничная адресация работает на уровне MMU. Но настраивается на уровне процессора.

Теперь все пространство поделяно на страницы(page) по 4Kb.

Страница описывается структурой из 4 байт PTE(page table entry).

Одна страница в которой лежат эти структуры ($2^{10}$ штук) называется Page Table.

Одна такая Page Table описывается структурой в 4 байта PDE(page directory entry)

И эти структуры лежат в Page directory, адрес на которую лежит в CR3. (Control register)

  • [10, 10, 12]
    • Первые 10 - индекс в page directory.
    • Вторые 10 - индекс в page table, адрес, который мы нашли в page directory.
    • Последние 12 - cмещение в page (не изменяется при трансляции)

У каждого процесса своя иерархия, у каждого есть своя ссылка на свою page directory.

Feature:

  • Каждая программа имеет в распоряжении 4Gb. Но физическое пространство такое же.

  • Есть куча флагов на разных уровнях иерархии, поэтому мы можем управлять памятью.

  • Ленивое выделение - страница реальная отобразиться в память когда она нужна.

  • Почти отсутсвие дефрагментации.

  • Разделяемые библиотеки. Разные процессы мапят одну и ту же память, которая содержит библиотеку. Каждый процесс мапит в свой виртуальные адрес. Эта память находиться в read-only. Если вдруг кто-то захочет поменять код библиотеки, то случается page-fault. Потом процессор копирует нужные страницы и ставит write permission. То есть в режиме copy-on-write.

  • swap(paging): swap - выгрузка всего дампа (сейчас редкость), paging - выгрузка части. Сброс страниц оперативки в память. Если страница долго не используется, то ОС выгружает ее, если мы хотим обратиться к ней, то она вгружается.

TLB: Мы получили очень гибкий механизм, но долгий. Поэтому сущетсвует TLB(translation lookaside buffer)-кеш.
Но не все так просто: при переключени ядра на другой процесс нужно его сбрасывать. При этом остальный кешы процессора (L1, L2, L3) сбрасывать не надо, так как они хранят только физические адреса.
Так же TLB можно использовать в уязвимости Meltdown.

Замечание: Традиционно первые сколько-то адресов не замапленые в никуда, это сделано, чтобы null-pointer падал гарантированно. Он просто идет в page directory и там написано, что не замапалено.

Замечание: Бывает спекулятивное подгрузка страниц. Можно посмотреть на Аиде, которая может вырубать это на работающем процессе, что посмотреть что поменяется.

Что делать если у нас оперативки больше, чем 4GB? Расширения:

  • PSE (page size extension): добавляется возможность создавать большие страницы по 4Mb. Для их меняется PTE, но не PDE, поэтому можно использовать и маленькие, и большие страницы.
  • PAE (physical address extension): Мы делаем структуру, которая описывает страницу жирнее не 32 бита а 64, чтобы адресовать больше адресов. Из-за этого меньше page table, поэтому есть еще один уровень. [31..30, 29..21, 20..12, 11..0] = [2, 9, 9, 12]
    • Это позволяет сильно увеличить ограничение на размер физической памяти.

Адресное пространство делиться на системное и не системное. Это определяет по первому старшему биту. Для 32 битных адресов: младшие 2ГБ - user space, старшие 2ГБ - kernel space.
Так сделано для удобства работы ОСи. Например, чтобы понимать у каких данных нужно проверять права доступа.

Замечание: Поэтому для 32битных приложений все равно полезно 64битная операционка, так тогда оно получит не 2ГБ, а 4ГБ.

Режимы работы процессора (64bit)

Зачем нужен 64bit?

  • Чтобы увеличить виртуальное адресное пространство.
    • Это важно при file-mapping.
    • При увеличении адресного пространства уменьшается его фрагментация.

Long mode (64битный мир)

От сегментной адресации не осталось почти ничего еще в 32bit режиме. Кроме:

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

Остальные сегменты получили базу 0, и лимит максимальный - 4Gb. Тем самым смещение виртуального адреса совпадает с линейным, который потом транслируется в физический через страницы.

Что с fs, gs? От них осталось поле базы.

  • fs -- обычно используется для thread local storage
    • Пример: winapp и может показать последнюю ошибку last_error(), но это работает и при нескольких тредов. Потому, что они хранятся локально для каждого треда.
    • fs указывает к каждом треде на разное смещение в TEB(thread enviroment block), из который уже узнается настоящий адрес переменной.
    • Linux используется для тех же целей gc
  • В Win32 - TEB32, Win64 - TEB64. Поэтому там используется и gc(64), и fs(32).

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

Режимы совместимости.

16/32 битный код не переключается обратно из long mode, для этого у него есть свои подрежимы(compatibility mode).

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

Как работает 32битная программа на 64 битной в Windows.

Есть системные библиотеки под каждую битность. Когда 32-битный код делает системный вызов, то вызывается NTDLL32.dll, которая делает far_call к NTDLL64.dll. Тем самым 32-битный код вызвает 64-битные системные вызовы.

Canonical address

Во всех текущих процессорах x64 не используются все 64 бита виртуального адреса. Так как пока $2^{64}$ слишком много для текущих требований, то из соображений оптимизации не все биты адреса транслируются. Только младшие 48 бит учавствуют в трансляции, что позволяет адресовать 256 TB. Учитывая деление виртуального пространства на user space и kernel space получаем доступный диапазон: 00000000'00000000..00007FFF'FFFFFFFF + FFFF8000'00000000..FFFFFFFF'FFFFFFFF

x32

Это подсистема(ABI) linux, которая позволяет работать в 64битном мире, но указатели размером 32бита. Иногда это полезно. Так тогда мы получаем полезности 64битного режима, но не платим за это размером указателей.

Картинка переключения режимов

  • EFER.LME - long mode enable
  • CR4.PAE - включить pae-расширение страничной адресации
  • CP0.PG - включить paging (возможности сбрасывать страницы в память)
  • CP0.PE - protected mode enable
  • EFLAGS.VM - virtual-8086 mode
  • CS.L - флаг дескриптора сегмента CS, который отвечает за 64-битность сегмента, при этом флаг D должен быть равен 0.

Замечание: Необходимо помнить, что переключени режимов - это привелегированная операция, а подрежимов - нет.

64-битный мир

История

AMD выпустили новую ISA, которая полностью поддерживала x86 и назвали ее AMD64. Потом Intel ее поддержал. Другие названия: x86-64, x64

Изменения по сравнению с x86

  • Расширение общих регистров до 64 бит. (rax, rbx ...) 32битные названия - это их младшие части.
  • у регистров esi, edi, ebp, esp появилась возможность обращаться к их младшей части. (rsi -> esi -> sih:sil)
  • новые регистры общего назначения r8..15 (r9 -> r9d -> r9w -> r9b) Замечание: TODO: более четко сформулировать
	mov al, bh % ok
	mov r9b, bh % нельзя, так как коды таких команды отданы под sil, dil

Адреса TODO

  • выражения в скобках [base_register + index_register * scale_factor + off32]
    • base_register - rax, rcx, ... r15
    • index_register - rax, ..., rsp, ..., r15
    • scale_factor - 1, 2, 4, 8
    • off32 - это знаковая константа. И ее битность являет проблемой. Метки перестают работать, так как их адрес стал 64битным, а в схеме мы не может это указать.
  • Можно использовать rip для указания смещения по коду.
    • mov reg, [rip + const32] TODO
    • mov reg [rel label], компилятор приводит это к тому, что выше.
    • написать default rel, чтобы дать указание компилятору все обращения к меткам приводить код строчке выше.

Так

  • Исключения из схемы
    • mov acc, [offset64] - только для al, ax, eax, rax.
    • mov r*, const64

Фича с обнулением

mov eax, ecx - обнуляет старшие 32 бита.

Это позволяет избежать больших констант, то есть больших кодов команд.

Замечание: У команды nop такой же код, как у команды xchg eax, eax. Поэтому в этом случае старшая часть не зануляется.

	xchg	eax, eax % нет зануления
	xchg	ebx, ebx % есть зануление

Замечание: Вместо xor rax, rax лучше писать xor eax, eax, так как занимает меньше места.

XMM

Добавили xmm8..15. А про mmx стараются забыть.

Выкинули команды.

  • aaa, aad, aam, aas, daa, das - 10ичная арифметика.
  • bound, into - что-то с проверкой границ.
  • pushad/popad - выгрузить все.
  • los/les - что-то с сегментами.
  • push/pop {cs, ds ...}
  • jmp/call для абсолютного адреса.
  • short form of inc/dec - теперь кодируется только в полную форму, старая занимала 1 байт.
  • sysenter/sysexit - intel команды для системного вызова.

Calling convention in x64

  • Во всех конвенция теперь обязательно перед вызовом функции выравнивать стек до 16.

Fastcall64 (Windows)

Порядок передачи аргументов.

  • rcx, rdx, r8, r9
  • xmm0, xmm1, xmm2, xmm3
f(int a, float b, int *c);
// a -> rcx
// b -> xmm1
// c -> r8

Возвращаемое значение

  • целое в rax или в rdx:rax для 128 бит.
  • вещественное в xmm0

Методы

this передается также(в нулевой аргумент)

сохраняемые регистры

rbx, rbp, rsi, rdi, r12..15, xmm6..15

Замечание: сохранение xmm большая проблема.

Фича: Shadow space

Она заключается в том на стеке всегда выделяется 32 байта вне зависимости от количества аргументов. Оно нужно для того, чтобы скинуть все аргументы на стек.

stack -> [ret][shadow space][other args]...

Это может быть полезно для variadic-функции, чтобы единообразно их обрабатывать.


без названия (Unix)

Порядок передачи аргументов.

  • rdi, rsi, rdx, rcx, r8, r9
  • xmm0..7 в rax записанно сколько загруженных xmm
f(int a, float b, int *c);
// a -> edi
// b -> xmm0
// c -> rsi
// rax = 1

Возвращаемое значение

  • целое в rax или в rdx:rax для 128 бит.
  • вещественное или комплексное в xmm0:xmm1

Методы

this передается также(в нулевой аргумент)

сохраняемые регистры

rbx, rbp, r12..15

Фича: Red zone

Она заключается в том, что мы можем писать на 128 байт ниже rsp.

Это может быть полезно для последний функции.

Про что здесь не сказано:

  • Локальные метки: может быть удобно, но компиляторозависимо.
  • Макросы: тоже, что и локальнымим метками.

НЛО TODO

1

NB) Есть две таблицы: - таблица, для всего, ее используется система. - урезанная таблица, для процесса. При syscall, мы переходим в код ядра, с полными таблицами.