Атрибуты сегментов при "абортном возврате" из защищённого режима.
Если в защищённом режиме загрузить в сегментный регистр (только
es, ds, fs, gs) селектор со следующими свойствами: это сегмент данных запрещённый для записи, или это сегмент кода разрешённый для чтения, то при возврате в реальный режим он таким и останется ! Пример (фрагмент программы реального режима).
xor bx,bx ;gs read only
mov cx,gs:[bx] ;Normal
mov gs:[bx],cx ;fault #GP
Дескриптор в ГДТ для этого примера был такой:
db 0FFh,0FFh,0,0,0,090h,0CFh,0
Такой работает аналогично :
db 0FFh,0FFh,0,0,0,09Ah,0CFh,0
При этом кажется странным следующее. Если записать селектор со свойствами: это сегмент кода, запрещённый для чтения в регистр cs (far инструкция), то при возврате в реальный режим он остаётся доступным для всего, и для чтения, и для записи. Дескриптор в ГДТ для кода был такой :
db 0FFh,0FFh,0,0,0,098h,0CFh,0 ;GDT descriptor for cs register
(Базу я записывал динамически.)
Хотя нет, это так и должно быть ...
Если в защищённом режиме загрузить в сегментный регистр (только
es, ss, ds, fs, gs) селектор со следующими свойствами: это сегмент данных, расширяемый вниз, то при возврате в реальный режим он таким и останется! Грузим в fs в защищённом режиме селектор дескриптора (db 0,0,0,0,0,096h,0C0h,0), возвращаемся в реальный режим и проверяем его свойства. Это 32-х битный сегмент данных, расширяемый вниз. Диапазон адресов которые он покрывает 00001000 - FFFFFFFF.
xor ebx,ebx
mov ax,fs:[ebx+1000h] ;Normal
mov cx,fs:[ebx+1000000h] ;Normal
mov dx,fs:[ebx+0FFEh] ;fault #GP
Все свойства унаследованы! Если бы он был 16 битным, то покрывал бы только 00001000-0000FFFF. Сегменты можно не только увеличивать, но и уменьшать. И маленькие сегменты тоже могут быть 32-х битными.
Страничная трансляция.
Вроде бы не работает в реальном режиме. Я переключил процессор в защищённый режим. Создал тождественное отображение памяти. Включил разрешение трансляции cr0.pg=1 . Потом изменил абсолютный адрес первой страницы по адресу 0A0000h (Видеопамять / пока в RM / mov ax,13h / int 10h) на следующую страницу. Заполнил 64000 байта с виртуального адреса 0A0000h байтом 001h. Верхняя часть экрана не окрасилась (работает трансляция) (без изменения абсолютного адреса первой страницы красился весь экран). Теперь, переключаясь обратно в реальный режим, я не обнулил бит cr0.pg и произошло страшное исключение с перезагрузкой компьютера. Была ещё надежда, что страничная трансляция будет работать и после обнуления бита cr0.pg пока не очистился TBL и не перезагрузился cr3, но нет
- не работает! При этом всём существует инструкция invlpg, заявленная только для защищённого режима работы процессора! Она
прекрасно выполняется в реальном режиме, но что она делает? Если операнд регистр тогда #UD. Иначе ничего не делать!? Она не проверяет даже на попадание в сегмент. И "invlpg cs:[00300000h]" нормально выполнится (лимит кода 0FFFFh).
Инструкции.
Есть замечательная недокументированная инструкция сопроцессора (Я её сам нашёл, но позже наткнулся в интернете на одну страничку и понял,
что её до меня уже давно нашли. Искал перебором свободных кодов, подсовывал их процессору, если не #UD значит хорошо.)
Её мнемоника:
ffreep st(i)
Её код:
DF C0+i
Её алгоритм:
ffree st(i)
fstp st(0)
Среди инструкций сопроцессора есть много алиасов инструкций. Три различных кода для fxch st(i): DF C8+i , DD C8+i , D9 C8+i . Два различных кода для fcom st(i) : DC D0+i , D8 D0+i . Три различных кода для fcomp st(i) : DE D0+i , DC D8+i , D8 D8+i . Четыре различных кода для fstp st(i) : DF D8+i , DF D0+i , DD D8+i , D9 D8+i .
Алиасы есть и среди целочисленных инструкций.
test rm,imm ; код 1111011w mod 001 r/m i8/16/32
В поле записано 001 - это алиас, а по документации только 000. Инструкции setccc игнорируют поле code (mod code r/m).
sal/shl действительно существуют два разных кода !!! соответствие полей следующее:
rol 000
ror 001
rcl 010
rcr 011
shl 100
shr 101
sal 110 <- этого поля нет в документации
sar 111
Поля для sal нет в документации и ему навязано значение shl. Хотя если записать инструкцию с полем 110, то она работает и именно так, как sal, это даже логично после того, как все поля записаны рядом.
Ещё есть хорошая недокументированная инструкция.
Её старая мнемоника:
icebp
И новая:
int1
Её код:
F1
Её алгоритм:
Если процессор поддерживает режим ICE и dr7.ir=1 (dr7.12 Interrupt
Redirectin), то войти в режим ICE (возможно есть более сложное условие). Иначе просто int 1. На современных процессорах всегда int
1 (при отладке программ я всегда ставлю бреак именно этим байтом.)
Как по вашему исполнится следующая инструкция ?
div edx
А точно также как и "div dx" или "div ah". Алгоритм: fault #DE
Доказательство:
test edx,edx
jz Очевидно
(qword ptr(edx:eax)) div (edx)=
=100000000h+((eax) div (edx))>0FFFFFFFFh
Результат в eax не влезет!
Что и требовалось доказать.
А как исполнится это ?
bswap cx ;или любой другой 2-х байтный регистр
;если сегмент 32 бит то код: 66 0F C9
;если сегмент 16 бит то код: 0F C9
После исполнения в cx будет ноль. Флаги не изменятся. Старшая половинка ecx не изменится. Возможны и такие инструкции, как "movsx r16,r16/m16" и movzx тоже.
Есть ещё интересная группа кодов, похожая на коды команды формата: mnemonic r32/16,rmem32/16
0F 19 mod reg r/m ... (Вместо 19 может быть любой байт из 19..1F)
Эти инструкции декодируются по такому формату и кажется ничего не делают? Пример (сегмент 32 бит):
0F 1F C2 ;mnem_1F eax,edx
67 0F 19 80 FE 02 ;mnem_19 ax,[bx+si+02FEh]
(это работает и на Pentium II)
Кажется инструкция smsw выдаёт cr0 целиком? (Windows
XP/user applicatin/segment 32 bit/Pentium 4 1.8 GHz)
smsw ebx ;0F 01 E3
ebx=8001003Bh
Операнд изменяется целиком! Все 4-е байта регистра. Регистр может быть любым. Если так, то вроде бы не врёт!
1000 0000 0000 0001 0000 0000 0011 1011
PCN- ---- ---- -A-W ---- ---- --NE TEMP
GDW- ---- ---- -M-P ---- ---- --ET SMPE
Очень правдоподобно! Видно ts=1 .Теперь исполняем инструкцию FPU и сразу считываем cr0. Получили следующее:
1000 0000 0000 0001 0000 0000 0011 0001
PCN- ---- ---- -A-W ---- ---- --NE TEMP
GDW- ---- ---- -M-P ---- ---- --ET SMPE
А ведь прямо считать cr0 пользователю невозможно! (mov ebx,cr0) Инструкция lar тоже выдаёт старшие биты лимита.
Про префикс repnz (0F2h)
repnz movsd ;работает абсолютно так же, как и это ниже.
rep movsd ;можно movs, lods,
ins, outs, stos, они ничего не сравнивают.
Кстати, Watcom генерит именно "неправильный" код для этого, первый вариант.
Инструкция fbld понимает расширенный формат BCD. Пример:
__ext_bcd dt 00AF37655FEBCD910CEAh
....
fbld tbyte ptr __ext_bcd
st(0)= 0Ah*(10^17)+
0Fh*(10^16)+
03h*(10^15)+
07h*(10^14)+
06h*(10^13)+
05h*(10^12)+
05h*(10^11)+
0Fh*(10^10)+
0Eh*(10^9)+
0Bh*(10^8)+
0Ch*(10^7)+
0Dh*(10^6)+
09h*(10^5)+
01h*(10^4)+
00h*(10^3)+
0Ch*(10^2)+
0Eh*10+0Ah
Есть также хорошая инструкция SALC с кодом D6. Заполняет битом CF
(флаг переноса / заёма) все биты регистра AL.
Инструкции bt/bts/btr/btc. И их свойства.
Если формат такой: btxxx word ptr memory,i8, тогда идёт попытка считать word по адресу memory. Если word был доступен как mov ax,word ptr memory, тогда продолжаем, иначе происходит исключение. А для bts/btr/btc он должен быть доступен и на запись! Из этого word вычитывается бит с номером (i8 and 0Fh), ну и заносится в cf.А, для bts/btr/btc он ещё и модифицируется. Если формат такой: btxxx dword ptr memory,i8,
тогда идёт попытка считать dword по адресу memory. Если dword был доступен как mov eax,dword ptr memory, тогда продолжаем, иначе происходит исключение. А для bts/btr/btc он должен быть доступен и на запись! Из этого dword вычитывается бит с номером (i8 and 01Fh)
и заносится в cf.А для bts/btr/btc он ещё и модифицируется.
Но самое интересное если формат такой: btxxx word ptr memory,r16. Тогда идёт попытка считать word по адресу memory+((r16 sar 4) shl 1) Если word был доступен как mov ax,(memory+((r16 sar 4) shl 1)), тогда продолжаем, иначе происходит исключение. А для bts/btr/btc он должен быть доступен и на запись! Из этого word
вычитается бит с номером (r16 and 0Fh) и заносится в cf.А для bts/btr/btc он ещё и модифицируется. Если формат такой: btxxx dword ptr memory,r32 Тогда идёт попытка считать dword по адресу memory+((r32 sar 5) shl 2) Если dword был доступен как mov eax,(memory+((r32 sar 5) shl 2)) тогда продолжаем, иначе происходит исключение. А для bts/btr/btc он должен быть доступен и на запись! Из этого dword вычитывается бит с номером (r32 and 01Fh) ну и заносится в cf.А для bts/btr/btc он ещё и модифицируется.
Если формат такой: btxxx r32,i8 Происходит операция с битом номер (i8 and 01Fh). Если формат такой btxxx r16,i8,
то происходит операция с битом номер (i8 and 0Fh). Если формат такой: btxxx r32,r32_bit Происходит операция с битом номер (r32_bit and 01Fh). Если формат такой: btxxx r16,r16_bit
- происходит операция с битом номер (r16_bit and 0Fh).
Пояснения:
Для форматов mem,reg нужно именно такое сложное вычисление адреса! Простое reg sar 3 не верно! Доказательство:
mov ax,111000b
btc word ptr [00803FF8h],ax ;Normal
Страница 00803000-00803FFF доступна на запись, а страница 00804000-00804FFF нет. Вычисляем адрес по которому будет считано слово (00111000b sar 4) shl 1 = 6 , 00803FF8h+6 = 00803FFEh. Полученное значение позволяет считать слово, а просто reg sar 3 даст 7 -> 00803FFF и это привело бы к ошибке. Больше увеличивать адрес нельзя.
mov ax,111000b
btc word ptr [00803FF9h],ax ;Fault
В вычислении адреса применяется sar т.к. битовое смещение интерпретируется как знаковое !
mov eax,-1
bts dword ptr [00803004h],eax ;Normal
bts dword ptr [00803003h],eax ;Fault
bts dword ptr [00803002h],eax ;Fault
bts dword ptr [00803001h],eax ;Fault
bts dword ptr [00803000h],eax ;Fault
btr word ptr [00803002h],ax ;Normal
btr word ptr [00803001h],ax ;Fault
btr word ptr [00803000h],ax ;Fault
Страница 00803000-00803FFF доступна на запись, а страница 00802000-00802FFF нет, но именно из неё были попытки читать в случае Fault. Ещё одна проверка! Создаём сегмент расширяемый вверх с лимитом 0FFFEh (на 1 байт меньше обычного).
mov ax,111000b
bt word ptr fs:[0FFF7h],ax ;Normal
bt word ptr fs:[0FFF8h],ax ;Fault
Хорошо и по нечётному адресу работает. Здесь 0FFF7h читают слово по адресу 0FFFDh,
а здесь 0FFF8h читают слово по адресу 0FFFEh, но оно уже не в сегменте. Какая хорошая инструкция !
__bit_string db 100000h dup (?)
.....
mov eax,400000h ;bit number
bt dword ptr __bit_string,eax
jc __bit_is_1
А как выполнится:
mov eax,1
xadd eax,eax
eax=2, но это не совсем очевидно!
Инструкция "shr eax,32" флаги не трогает! Инструкции "mov from/to system register" игнорируют поле mod. Список таких отклонений можно ещё продолжить ...
Примечание: leave работает по следующему алгоритму:
if (StackAddressSize=32) then esp<-ebp
else (* StackAddressSize=16 *) sp<-bp;
if (CurrentOperandSize=32) then pop(ebp)
else (* CurrentOperandSize=16 *) pop(bp);
И в случае когда разрядность стека 32, а текущая 16 будет происходить частичная потеря информации. enter создаёт пролог не соответствующий leave. Пример
(Windows\model flat):
mov ebp,803400h
mov esp,12FFC4h
siz enter 0,0
siz leave
ebp=0080xxxxh ;xxxx - то что лежало по адресу ss:0012FFC2h .
esp=0080FFC4h ;
Это исполняется так ...
push bp
mov bp,sp ;какой "enter" плохой !!!
mov esp,ebp
pop bp
Атрибуты сегментов в реальном режиме используются,
но немного по другому их воспринимает cs.
Инструкция aaa.
mov eax,0FFh
aaa
Теперь в eax значение 0205h, а по документации должно быть
0105h (увеличивается на 6 не al, а ax, правда это только для 386+,
а для 286 документация верна).
Инструкция aas тоже работает с ax.
xor ecx,ecx
mov ah,10000b ;af=1
sahf
mov eax,ecx
aas
eax=0000FE0Ah .
Инструкция aam i8 делит без знака. В документации очень долго умалчивалось, что она может иметь операнд, скрытым был операнд равный 10, но оказывается может быть любой. Результат деления в ah, а остаток в al.
mov al,0FFh
aam 2
Теперь ax=7F01h.
Windows XP (ntoskrnl.exe).
Я нашёл там следующие инструкции:
loadall (0F 05 - старый вариант для 286)
cpuid
cmpxchg8b (Хоть здесь она есть)
sysenter (Так осуществляется переход на сервисы OS)
sysexit (А так обратно)
sfence
xorps
movntps
fxsave
fxrstor
movaps
prefetchnta
movnti
pause
loadall там на случай если CPU не поддерживает sysenter/sysexit и именно старый. Он должен вызывать исключение по которому и отслеживается вызов сервиса OS (если это loadall). Я нашёл такой фрагмент кода (зануляют память с
помощью xmm регистра командами SSE):
xorps xmm0,xmm0
mov eax,40h
__fill:
movntps [ecx],xmm0
movntps [ecx+10h],xmm0
movntps [ecx+20h],xmm0
movntps [ecx+30h],xmm0
add ecx,40h
dec eax
jnz __fill
sfence
Был и такой фрагмент:
mov eax,[esi]
mov ebx,[esi+4]
movnti [edi],eax
movnti [edi+4],ebx
mov eax,[esi+8]
mov ebx,[esi+12]
movnti [edi+8],eax
movnti [edi+12],ebx
И такой:
prefetchnta [esi]
or ecx,ecx ;Это лишняя проверка
jz __skip
rep movsb
__skip:
Инструкции bt/bts/btr/btc тоже используются. Ими считывается и устанавливается удалённый бит. Вот код функции из "ntoskrnl.exe":
push ebp
mov ebp,esp
push esi
xor eax,eax
push eax
push eax
push eax
push eax
push eax
push eax
push eax
push eax
mov edx,[ebp+0Ch]
lea ecx,[ecx]
lab_2:
mov al,[edx]
or al,al
jz short lab_1
inc edx
bts [esp],eax ;<<--
jmp short lab_2
lab_1:
mov esi,[ebp+08h]
or ecx,-1
lab_4:
inc ecx
mov al,[esi]
or al,al
jz short lab_3
inc esi
bt [esp],eax ;<<--
jc short lab_4
lab_3:
mov eax,ecx
add esp,20h
pop esi
leave
retn
Всё используется!