Осторожно этот конспект был написан одним человеком, и может содержать ошибки и не человекочитаемые фразы.
Если вы нашли баги в конспекте, то напишите мне. Я буду готов, что-то поправить до ноября.
- Лекции Скакова П. С.
- http://fat-crocodile.narod.ru
- https://www.felixcloutier.com/x86/
Ассемблер
-- это не язык программирования. Это мнемоника команд процессора. Из этого следует что, сколько существует 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
- www.agner.org - можно найти информацию о производительности на разных архитектурах.
- www.aida64russia.com - инструмент для тестирования производительности.
- справка по командам
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 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] -- знаковое сравнение
- c/z/s -- если соответствующий флаг установлен:
cmov__
-- условная загрузка. Суффикс как выше. Не предсказывается.
Практически все арифметически операции меняют флаги. Подробности в документации, или здесь
add/adc
-- сложение/сложение + флаг переноса. (carry flag)sub/sbb
-- вычитание/вычитание - флаг переноса.mul rx
-- беззнаковое умножение.mul r8
=> ah:al = ah * r8mul r16
=> dx:ax = ax * r16mul r32
=> edx:eax = eax * r32
imul
-- знаковое умножениеimul reg
=> как вышеimul dst, src
=> dst *= srcimul 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
-- команда, которая гарантированно отсутствует.
test eax, eax
jns L1
neg eax
L1:
% но лучше так (нет условных переходов):
cdq
xor eax, edx
sub eax, edx
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(eax != 0);
Некоторые конструкции языков C/C++ существуют именно в таком виде, потому что так их можно эффективно реализовать.
L1:
...
test eax, eax
jnz L1
while(eax != 0)
...
jz L2
L1:
...
cmp eax, 0
jnz L1
L2:
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
lea eax, [eax*4 + eax]
add eax, eax
TODO
section .text
- text -- код.
- data -- данные.
- rdata -- данные только для чтения.
- bss -- неиницализированные данные. Почти не занимаю место в исполняемом файле.
Эти секции описывают свойства процесса. Не путать с сегментами! Они про адресацию.
При написание высокоуровнего кода мы этого почти не замечаем. Но если мы хотим написать код на ассемблере и вызвать его где-то еще, то важно знать как буду вызвать наш код, и как вызвать чужой код. Для этого существуют соглашения (которые можно подкрутить компиляторозависимыми ключами)
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 | ⬅️ | ⬅️ |
Замечание: Сохранять все регистры, также как и не сохранять ничего -- дорого.
Пояснение: Пусть мы сохраняем все регистры: тогда в начале тела функции нужно сохранить все регистры, которые мы хотим использовать. Если мы не будет вообще сохранять регистры, то после вызова внешней функции нельзя пользоваться значениями в регистрах, поэтому опять надо все сохранять.
Компилятор генерирет специальный код: выделяет необходимое количество памяти под возвращаемое значение и передает его адрес в качестве нулевого аргумента. При чем выделять память под возвращаемое значение должен вызывающий код, так как память нужно освобождать ровно так, как она выделялась.
Это нужно для методов классов. Тут тоже пытаются класть 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
В stdcall не возможно написать printf, так как это vararg-функция. Такие функции требуют:
- стек должен чистить caller, так как функция не может знать сколько аргументов ей передали.
-
Конвенция вызова должна совпадать с конвенцией фукнции. Но это неважно для локальных фукнций, которые никто не будет вызвать, кроме вас.
-
если мы хотим сделать рефакторинг кода на Ассемблере, например, помнять логику программы и делаем
push
в середине кода, то все адреса стека, к которым обращаются ниже нужно изменить, так как стек сдвинулся. Поэтому удобно писать адрес обращения к аргументам функции из двух часте:mov eax, [esp + 4 + 4]
, первая цифра -- номер аргумента, вторая -- сколько мы пушнули. -
Почитать Агнер Calling convention.
TODO
: добавить ссылку.
Исторически это сопросцессор, который был сильно разнесен с CPU. Поэтому нет прямого доступа между их регистрами, только через память.
У сопроцессора есть свои регистры: 8 регистров, которые собраны в зацикленный стек.
st[0..8]
- 80 бит (extended presion)
- Поддерживает разные типы:
- dword(одинарная точность)
- qword(двойная точность)
- tbyte(extended точность)
- BCD80(что-то с десятичной арифметикой)
fld
(from float)fild
(from integer)fbld
(bcd80)fldpi
/fld1
/fldz
(loadpi
/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
-- выгружает значение из регистра и выкидывает его со стек. При этом значение конвертится к целому числу отбрасыванием дробной части.
- Вызванная функция должна зачистить стек командой
finit
перед использованием x87. - Если возвращаемое значение функции имеет вещественный тип, то оно кладется на вершину стека (st0). Это верно для любой конвенции 32битного мира.
- про конвертацию между целыми и вещественными числами: (касается не только 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
- single instruction, multiple data. Это идея когда мы можем одной командой обрабатывать сразу несколько ячеек данных.
- 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)
.
Так как эти регистры накладываются на регистры x87, их нельзя использовать одновременно. Во время работы с mmx
, все st
помечаются как недоступные, и в конце нужно вызвать команду emms
, которая пометит все st0..8
, как доступные к использованию.
Замечание: Эта команда сильно похожа на finit
, но последнаяя также устанавливает дефолтное значение.
-
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
- на стеке
section .data
align 16
const1: 12
- в куче
alloc
+ ручное выравниваниеalign_alloc
(Linux)/virtual_alloc
(Windows)
Перед любым вызовом функции в 32 битах жилательно выравнивать стек до 16, а в 64 битности - это обязательно.
push ebp
mov ebp, esp
sub esp, n
and esp, -16
...
mov esp, ebp
pop ebp
TODO
Замечание: Почему важна обратная совместимость?
Потому, что старый код очень долго переписывать, и новые технологии внедряются не мнговенно. При этом программная эмуляция очень сильно проседает по скорости.
Режим работы меняет коды команд.
- real 16
- protected 16/32/v86
- long mod 64/compatibility32/compatibility16
Биос начинает работать в real mode и потом постепенно переходит в нужный.
Изначально в этом режиме работали 16 битные процессоры.
В i80186 можно адресовать 1 Мб оперативки, то есть
Физический адрес
= сегмент
* 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 как базы
Может адресовать 24-битные адреса.
Добавились новые сегментные регистры: fs
, gs
с неопределенным использованием.
A20 line: в i80186 при обращении за границы 1 Мб происходило переполнение. То есть всегда использовались только последние 20 бит.
А в i80286 можно адресовать большую память, поэтому переполения не происходило и можно было адресовать real mode
. Для обратной совместимости можно влючить переполнение. Про это Bios Line 20 disable
- Нельзя управлять памятью.
- Невозможно безопастно делить ее между процессами.
- Отделять код системы от кода пользователя.
Это режим появился в 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 бита, поэтому максимальная доступная память -
- Ближние jump, call -- в приделах одного сегмента памяти.
- Дальние: far_jump, far_call -- меняет также сегмент кода.
jmp segment:offset
Это идея обращения процесса с памятью. Она заключается в том, что память для процесса - это единое и не делимое пространство.
Это режим появился в 386. В нем увеличилась битность линейного адреса до 32 бит и появилась возможность делать сегменты с лимитом 4Gb. Это открыло доступ к Flat-module работы с памятью.
Размер дескриптора при этом не поменялся, так как в нем заранее присутствовали 2 неиспользуемых байта.
Также в дескрипторе есть флаг, который для сегмента кода значит, какой битности записаны команды: 16 или 32. (D-bit)
Подробнее о флагах на вики
Замечание: Теперь лимит не может принимать любые значения, так как он занимает всего 20 бит и увеличивается за счет изменения гранулярности с 1 байта на 4Кб.
Существует незадокументированная фича, что при переходе в real mode
теневая часть не меняется. Это дает доступ к сегментам большого размера в реальном режиме. Это называют нереальном режимом. (unreal mode)
Так как в real mode можно использовать 32 битные регистры, то можно получить доступ к 4Gb. Это фича называется Operand Size Override Prefix
и включается отдельно(может добавляться автоматически компилятором ассемблера). И важно отключить вентиль A20.
TODO: Проверить: Видимо лимит на размер сегмента в реальном моде даже на i80186 хранился в скрытой части сегмента(видимо тогда она уже сущетсвовала.). Но я не нашел явно упоминания.
возможно лимит не надо было хранить, так как он всегда один и тот тоже.
Режим совместимости с 8086. Адресация как в реальном режиме, но с кольцами прав.
System Management Mode - режим отладки. wiki
Это новый способ адресации, который был введен с 386.
Замечание: Почему на некоторых Win32 нельзя выделить больше 4 ГБ?
Это лишь лицензионное ограничение. На самом деле благодаря страничной адресации мы можем выделять довольно большое адресное пространство для программы. Так как система сама строит адресное пространство.
AWE
- другой хак, который позволяет выделять процессу больше чем 4ГБ без страничной адресации.
Замечание: AWE
(Address Windowing Extension) - суть заключается в окне в адресном пространстве программы, которое мы можем отображать в разные части пространства физических адрессов.
Замечание: Страничная адресация работает на уровне MMU. Но настраивается на уровне процессора.
Теперь все пространство поделяно на страницы(page) по 4Kb.
Страница описывается структурой из 4 байт PTE(page table entry).
Одна страница в которой лежат эти структуры (
Одна такая 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?
- Чтобы увеличить виртуальное адресное пространство.
- Это важно при file-mapping.
- При увеличении адресного пространства уменьшается его фрагментация.
От сегментной адресации не осталось почти ничего еще в 32bit режиме. Кроме:
- в
cs
осталось кольцо прав - в дескрипторе
cs
указывается битность сегмента, которая определяет битность режима.
Остальные сегменты получили базу 0
, и лимит максимальный - 4Gb. Тем самым смещение виртуального адреса совпадает с линейным, который потом транслируется в физический через страницы.
Что с fs
, gs
?
От них осталось поле базы.
fs
-- обычно используется для thread local storage- Пример: winapp и может показать последнюю ошибку
last_error()
, но это работает и при нескольких тредов. Потому, что они хранятся локально для каждого треда. fs
указывает к каждом треде на разное смещение в TEB(thread enviroment block), из который уже узнается настоящий адрес переменной.- Linux используется для тех же целей
gc
- Пример: winapp и может показать последнюю ошибку
- В Win32 - TEB32, Win64 - TEB64. Поэтому там используется и
gc
(64), иfs
(32).
Страничная адресация: Идейно ничего нового, просто больше страниц, больше уровней, возможность делать еще большие страницы в PSE.
16/32 битный код не переключается обратно из long mode, для этого у него есть свои подрежимы(compatibility mode).
Переход в другую битность - это просто far jump. При этом прогружается другой сегмент, с другой битностью. Но чтобы все реально работало есть много тонкостей, поэтому часто просто стараются избежать разнородной битности в коде.
Есть системные библиотеки под каждую битность. Когда 32-битный код делает системный вызов, то вызывается NTDLL32.dll, которая делает far_call к NTDLL64.dll. Тем самым 32-битный код вызвает 64-битные системные вызовы.
Во всех текущих процессорах x64 не используются все 64 бита виртуального адреса.
Так как пока 00000000'00000000..00007FFF'FFFFFFFF + FFFF8000'00000000..FFFFFFFF'FFFFFFFF
Это подсистема(ABI) linux, которая позволяет работать в 64битном мире, но указатели размером 32бита. Иногда это полезно. Так тогда мы получаем полезности 64битного режима, но не платим за это размером указателей.
EFER.LME
- long mode enableCR4.PAE
- включить pae-расширение страничной адресацииCP0.PG
- включить paging (возможности сбрасывать страницы в память)CP0.PE
- protected mode enableEFLAGS.VM
- virtual-8086 modeCS.L
- флаг дескриптора сегмента CS, который отвечает за 64-битность сегмента, при этом флагD
должен быть равен 0.
Замечание: Необходимо помнить, что переключени режимов - это привелегированная операция, а подрежимов - нет.
AMD выпустили новую ISA, которая полностью поддерживала x86 и назвали ее AMD64. Потом Intel ее поддержал. Другие названия: x86-64, x64
- Расширение общих регистров до 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
- выражения в скобках
[base_register + index_register * scale_factor + off32]
base_register
-rax
,rcx
, ...r15
index_register
-rax
, ...,, ...,rsp
r15
scale_factor
- 1, 2, 4, 8off32
- это знаковая константа. И ее битность являет проблемой. Метки перестают работать, так как их адрес стал 64битным, а в схеме мы не может это указать.
- Можно использовать
rip
для указания смещения по коду.mov reg, [rip + const32]
TODOmov 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
, так как занимает меньше места.
Добавили 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 команды для системного вызова.
- Во всех конвенция теперь обязательно перед вызовом функции выравнивать стек до 16.
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
большая проблема.
Она заключается в том на стеке всегда выделяется 32 байта вне зависимости от количества аргументов. Оно нужно для того, чтобы скинуть все аргументы на стек.
stack -> [ret][shadow space][other args]...
Это может быть полезно для variadic-функции, чтобы единообразно их обрабатывать.
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
Она заключается в том, что мы можем писать на 128 байт ниже rsp
.
Это может быть полезно для последний функции.
- Локальные метки: может быть удобно, но компиляторозависимо.
- Макросы: тоже, что и локальнымим метками.
NB
) Есть две таблицы:
- таблица, для всего, ее используется система.
- урезанная таблица, для процесса.
При syscall, мы переходим в код ядра, с полными таблицами.