- Ngôn ngữ Assembly, các loại kiến thức cơ bản về thanh ghi (registers), truy cập vào bộ nhớ (memory access), các lệnh (command), ...v.v
- Flat Disassembler dùng để tạo executable file trên windows
- Bochs debugger
Assembly (hay còn được gọi là hợp ngữ) là 1 tập hợp các instruction (câu lệnh được dịch theo mã máy(opcodes)) được dùng để làm 1 vài thứ tiện ích và để truy cập vào bộ nhớ của hệ điều hành.
Vì bộ nhớ không được xem như là 1 chuỗi các bytes liên tục (như 1 chuỗi trong ngôn ngữ C) nên nó được chia thành các phân khúc (segments). 1 segment nó sẽ có nhiều nghĩa khác nhau, tùy thuộc vào CPU đang hoạt động ở chế độ nào (Real, Protected, Long). Địa chỉ bộ nhớ được chỉ định bởi thanh ghi segment (segment register - là nơi mà giữ giá trị của segment), còn độ dời (offset) chỉ khoảng cách từ khi bắt đầu cho đến 1 segment.
Để cho việc tạo các biến local (local variables) vào các functions (hàm) (như trong C++) và chuyển đổi dữ liệu giữa chúng, mỗi ứng dụng sẽ được cài đặt một segment đặc biệt được gọi là "stack segment" - nơi dùng để chứa địa chỉ của bộ nhớ được dùng cho stack. Stack được áp dụng theo phương pháp "LIFO": (Last In First Out) - thằng nào được push
vào cuối cùng thì sẽ được pop
ra đầu tiên.
Giống như con trỏ trong ngôn ngữ C, nếu bạn có 1 variable chứa con trỏ trong C, bạn có thể dùng *
để truy cập dữ liệu. Còn trong Assembly, bạn có thể làm tương tự với []
:
; assume that DS has the data segment
mov si,000fh
mov dx,[si] ; Now dx has whatever was placed in DS:000fh
Nếu như bạn truy cập một cái gì đó nó không hề tồn tại thì sao? trong DOS, bạn có thể trash yourself or the OS or both
(có thể hiểu là loại bỏ); ở Protected mode, bạn sẽ không được phép truy cập vùng nhớ không có thực (non-existent memory) và vì vậy, exception handler sẽ được gọi (nếu không được gọi, chương trình coi như xong).
Giống như trong C++, trong assembly vẫn có function calls (không phải mọi thứ đều được gọi label goto
). Phụ thuộc programming model (sẽ được biết sau), 1 function có thể là near
(cái mà thanh ghi IP
(instruction pointer) được push vào stack và hàm exits cùng với RET
), far
(cái mà IP và code segment (CS) được push, và hàm exits với RETF
) hoặc interrupt
.
interrupt
cơ bản được xem như 1 handler và execute khi có 1 hàm nào đó gọi nó. thường thì chương trình gọi nó thông qua lệnh INT
. Mọi dịch vụ DOS/BIOS được cung cấp thông qua interrupt
, nên nếu ta thấy:
; assume that DS has the data segment
mov ah,9
mov dx,msg;
int 21h;
thì ta biết được rằng nó gọi hàm 9
của INT 21h
- là 1 hàm DOS dùng để hiển thị 1 chuỗi (string) được trỏ bởi DS:DX (Data segment : data register). Địa chỉ của mỗi interrupt (255 địa chỉ) được chứa trong interrupt Table, có thể truy cập được thông qua lệnh SIDT
, nó sẽ rất khác biệt và phụ thuộc vào chế độ của bộ xử lí (Processor mode). IP
, CS
và các loại cờ được push vào stack, và interrupt exits với IRET
.
cũng có một số vài trường hợp khác interrupt được thực hiện. VD: khi chúng ta code lấy 1 cái gì đó chia cho số 0
, INT 00h
sẽ tự động được gọi. dòng INT 00h
thấy bạn đã thử chia cho số 0, nên nó sẽ dừng chương trình không tiếp tục nữa. Sau đó Windows sẽ báo lỗi và đóng chương trình.
nếu bạn có thể tự cài đặt INT 00h
handler (thông qua hàm DOS 25h
), thì exception sẽ thực hiện dòng code trước khi cửa sổ Windows hiện ra, nhưng mình vẫn phải fix lỗi đó. Nếu không sẽ như đã nói trên.
Trên DOS, exception handler mặc định thường rất ít tác dụng lên CPU để chặn nó tiếp tục thực hiện code, vì vậy bạn chỉ có thể mở Task Manager (Ctrl + Alt + Del).
AX
BX
CD
DX
SI
DI
BP
SP
IP
- 16-bit cờ (flag) register
IP
chứa địa chỉ kế tiếp sẽ thực hiện dòng code. khi step qua, IP
sẽ tự động thay đổi.
AX
, BX
, CX
, DX
có thể truy cập như thế này:
mov ax,1 ; ax is now 1
mov cx,ax ; cx is now also 1
hoặc sử dụng 8 bit cao (low-bit)[ah
, bh
, ch
, dh
] hoặc 8 bit thấp (high-bit)[al
, bl
, cl
, dl
]
xor ax,ax ; ax is now 0
mov ah,1 ; ah is now 1, thus ax is now 0100h
mov al,2 ; al is now 2, thus ax is now 0102h
SI
, DI
luôn được xem như là thanh ghi 16-bit (không có sl
, sh
) và thường được dùng như một con trỏ trỏ vào data. BP
được dùng như một thanh ghi dùng chung - generic-purpose register - (mặc dù nó thường được dùng để access vào stack), và SP
giữ giá trị của trỏ trỏ vào đỉnh của stack. Vậy hãy xem thử stack như thế nào khi ta push cái gì đó vào stack:
; assume that SP has the value of 0100h
mov ax,3 ; ax is now 0
push ax ; ax is put to the stack, SP has now the value of 00FEh; (100h - 2)
mov dx,5 ; dx is now 5
push dx ; dx is put to the stack, SP is now 00FCh;
pop bx ; bx gets the value from the stack top (5). SP gets back to 00FEh
pop cd ; cx gets the value from the stack top (3). SP gets back to 0100h
vậy nếu như ta push nhiều hơn lưu lượng mà stack có thể chứa được thì nó sẽ như thế nào? BANG, Stack Overflow
.
Flags register
được set là 16 bits (mặc dù không dùng hết), nó sẽ thay đổi giá trị tùy thuộc vào các hoạt động tính toán, các biểu thức logic (operations) qua mỗi dòng opcode. Các kiểu rẻ nhánh của lệnh JMP
(JZ
, JAE
, JB
, ...v.v) có thể jump
theo điều kiện phụ thuộc vào các cờ trên. Ví dụ, cờ ZF
(zero flag) sẽ được set 1
khi operation thực hiện cho kết quả zero:
mov ax,bx ; Get value of BX to AX
or ax,ax ; Is AX 0? If yes, or ax,ax will also say 0, so ZF will be 1
je AxIsZero
jmp AxIsNotZero
bạn có thể dùng pushf
hoặc popf
để set hoặc đọc flags vào thanh ghi, VD:
pushf ; push flags to stack
pop ax ; ax has now the flags
or al,1 ; set the bit 0 to 1
push ax
popf
CS
DS
SS
ES
FS
GS
các loại thanh ghi này giữ những giá trị dùng để xác định segment hiện tại. Cách mà giá trị này được biên dịch phụ thuộc vào CPU mode hiện tại (Real/Protected/Long).
CS
luôn giữ segment của dòng code đang được thực hiện. Bạn không thể thay đổi giá trị của CS
bằng việc mov cs,ax
. Khi bạn gọi một hàm đang ở trong 1 segment khác (FAR
call), or là khi bạn jump đến 1 segment (FAR
jump), thì CS
thay đổi.
DS
giữ giá trị mặc định của data segment. Có nghĩa là nếu bạn làm như thế này:
mov si,0
mov ax,[si]
mov bx,[1000h]
khi AX
lấy giá trị từ segment được trỏ bởi DS
, với 1 offset đặc trưng bởi SI
,và BX
lấy giá trị được trỏ bởi DS
với offset là 1000h
. Nếu bạn muốn dùng một segment khác, bạn phải làm như sau:
mov di,0
mov ax,[fs:di]
mov bx,[es:1000h]
khi DS
được dùng như một index, ES
sẽ giữ segment mặc định. Khi BP
được sử dụng như một index, SS
sẽ là segment mặc định. các trường hợp khác thì DS
sẽ là segment mặc định. Chú ý rằng không nhất thiết phải mọi thanh ghi sẽ được dùng như 1 index ở real mode, ví dụ: mov ax, [dx]
sẽ không thể thực hiện được ở real mode.
ES
, FS
, GS
là các thanh ghi segment phụ trợ chung (general-purpose auxiliary segment registers). SS
giữ giá trị của stack segment.
có thể sử dụng được ở mọi mode (Real, Protected, Long)
EAX
EBX
ECD
EDX
ESI
EDI
EBP
ESP
EIP
- 32-bit Flags Register
mỗi thanh ghi trên là phần mở rộng của thanh ghi 16-bit tương ứng. VD:
mov eax,0 ; eax is now 0, so ax is also 0.
mov ax,1 ; ax is now 1, eax is also 1
or eax,0FFFF0000; ax is now 1, eax is now 0FFFF0001h
32-bit Register dùng được trong Real mode, nhưng index (EDI
và ESI
) không thể dùng được nếu 16 bit cao của nó là 0
(bạn chỉ có thể dùng tối đa index 65535). Nhưng sẽ có cách để thông qua điều này trong phần Real mode.
chỉ có thể áp dụng được nếu processor ở chế độ 64-bit. Không thể dùng được trong Real, Protected, Compatibility mode
RAX
RBX
RCX
RDX
RSI
RDI
RBP
RSP
RIP
- 64-bit Flags Register
64-bit mode sẽ thêm vào 8 thanh ghi 64-bit (từ r8 --> r15) để dùng như một thanh ghi phụ trợ (auxiliary registers), 1 vài thanh ghi 128-bit giúp cho việc lập trình trên nhiều thiết bị khác nhau (programming multimedia)
CR0
: dùng để set CPU về Protected mode(bit 0), và bật chế độpaging
(bit 31).CR1
: dự phòng (reserved)CR2
: giữ giá trị địa chỉ củaPage Fault Linear
khi 1 page fault exception được bật lênCR3
: giữ giá trị địa chỉ của bảng paging (paging table)CR4
: dùng để định dạng một vài flag khác, như Physical Address Extensions và VM86 mode
những thanh ghi này lưu giữ giá trị hiện tại (current state) của CPU. đọc thêm Control Registers để biết thêm thông tin.
DR0
DR1
DR2
DR3
DR6
DR7
những thanh ghi này chứa giá trị của hardware debugging. DR0
- DR3
chứa giá trị của địa chỉ gần(linear address) của 4 breakpoint, và DR6
, DR7
cần một vài flag để có thể dùng. Debug Registers để biết thêm thông tin.
TR4
TR5
TR6
TR7
những thanh ghi này giữ thông tin về CPU trong việc testing (hiện nay không còn dùng nữa). Test Registers để biết thêm thông tin.
trong Real mode, mọi thứ đều là 16 bits. Cả một bộ nhớ (memory) không chắc chắn được truy cập với các giá trị index từ 0, nhưng nó được chia thành nhiều segments. Mỗi segment nó đại diện cho một offset từ 0
, rồi sau đó nhân cho 16
. Ở trong segment này, offset được coi như là khoảng cách từ lúc bắt đầu segment. cả 2 thứ [segment
:offset
] cho CPU biết được giá trị tuyệt đối (absolute value) của bộ nhớ khi ta cần truy cập vào nó. Ví dụ:
0000h : 0000h
: segment = 0, offset = 0. 0*16 + 0 = 0 là địa chỉ bộ nhớ thực (actual memory address).0100h : 000Fh
: segment = 100h, offset = 0Fh. 100h*16 + 0Fh = 100Fh là actual memory address.0002h : 0000h
: 02h*16 + 0 = 20h là actual memory address.0001h : 0010h
: 01h*16 + 10h = 20h. Như ta thấy thì memory address có thể lặp lại (overlap).
vì segment và offset đều 16 bits, nên giá trị tối đa mà bộ nhớ có thể truy cập được là 0xffff
: 0x10
= 1MB
. nếu lớn hơn thì sẽ dẫn đến việc bị wrapping
(xem ở Protected mode, dòng A20). Và vì các vị trí sau 0xa00 : 0x0
được dùng cho hệ thống (màn hình , ...v.v), nên chỉ còn lại 640KB
cho các ứng dụng DOS.
Thêm vào đó, tất cả các segment đều có đủ các quyền truy cập read/write/execute
từ bất cứ đâu (nghĩa là bất cứ chương trình nào đều có thể read/write/execute
được code bên trong bất cứ segment nào).Because in 16-bit real mode OS the CPU sees the memory the way above, any application can read from or write to any part of memory, including the part in which the OS resides. That is why a real mode OS is a single tasking OS.
(đoạn này đọc = tiếng anh sát nghĩa hơn)
ở Real mode, CS:IP
chứa execution point hiện tại (giống như IP
), DS
chứa giá trị data segment mặc định, SS
chứa giá trị stack segment. Bất cứ ứng dụng nào lớn hơn 64K code hay data segment thì bắt buộc phải xé ra thành nhiều segment riêng.
interrupts hiểu đơn giản là 1 hàm đặc biệt sẽ được gọi khi xảy ra cái gì đó (hay còn được gọi là hardware interrupts), giống như việc chia cho 0
, or khi được gọi bởi phần mềm (thông qua việc dùng INT
instruction - hay còn được gọi là software interrupts). Ở Real mode, có tới 256 loại interrupts. Bảng chứa segment:offset
cho mỗi interrupts được đặt đầu tiên ở absolute address 0
, nhưng (với 286 + interrupts) có thể sẽ đặt nó ở đâu đó khi dùng LIDT
instruction (dùng SITD
để lấy table address).
ở Real mode, OS cung cấp các tính năng cho ứng dụng thông qua software interrupts, chẳng hạn DOS sẽ cung cấp độ rộng của các hàm thông qua INT 21h
.
chương trình sẽ được nạp vào memory segment bởi DOS, và việc thực thi (execution) bắt đầu ở offset của EXE's
header (hoặc ở 0x100
nếu nó là file COM
- file không có header). Sau đó, ứng dụng được quyền làm bất cứ điều gì, để có thể làm trống được bộ nhớ. Đây chính là chức năng
đặc biệt của Real mode: chỉ một ứng dụng mà có thể sở hữu cả một cỗ máy. thêm vào đó, ứng dụng được cho phép giao tiếp trực tiếp với bất cứ phần cứng (thông qua in/out opcodes), vì vậy việc vượt rào (bypass) bất cứ giới hạn bảo mật nào của OS. Và nếu ứng dụng bị crash, cả một hệ thống sẽ bị crash theo và ta buộc phải reboot lại máy.
đây là đoạn code "Hello World" đơn giản ở 16-bit EXE, dùng nhiều segment:
FORMAT MZ ; DOS 16-bit EXE format
NTRY CODE16:Main ; Specify Entry point (i.e. the start address)
STACK STACK16:stackdata ; Specify The Stack Segment and Size
SEGMENT CODE16_2 USE16 ; Declare a 16-bit segment
ShowMsg:
mov ax,DATA16
mov ds,ax ; Load DS with our "default data segment"
mov ax,0900h
mov dx,Msg
int 21h; ; Call a DOS function: AX = 0900h (Show Message),
; DS:DX = address of a buffer, int 21h = show message
retf ; FAR return; we were called from
; another segment so we must pop IP and CS.
SEGMENT CODE16 US##E16 ; Declare a 16-bit segment
ORG 0 ; Says that the offset of the first opcode
; of this segment must be 0.
Main:
mov ax,CODE16_2
mov es,ax
call far [es:ShowMsg] ; Call a procedure in another segment.
; CS/IP are pushed to the stack.
mov ax,4c00h ; Call a DOS function: AX = 4c00h (Exit), int 21h = exit
int 21h
SEGMENT DATA16 USE16
Msg db "Hello World!$"
SEGMENT STACK USE16
stackdata dw 0 dup(1024) ; use 2048 bytes as stack. When program is initialized,
; SS and SP are automatically set.
làm sao trình dịch có thể biết được giá trị thực của data16
, code16
, code16_2
và stack16
segment? thực chất không hề. Việc mà nó làm chính là cho giá trị NULL
- A.K.A \x00
- và sau đó tạo đầu vào (entry) đến file EXE (được hiểu như là tái vị trí - relocation) nên chương trình nạp (loader) sẽ copy code vào trong bộ nhớ, ghi một vài địa chỉ đặc trưng - giá trị thực của các segment. Bởi vì bản đồ relocation có header, các file COM không thể chứa được nhiều segment cho dù tổng dung lượng của nó thấp hơn 64KB.
chương trình trên gọi hàm ShowMsg
ở một segment khác thông qua far call
, cái mà sử dụng hàm DOS (0x9
, INT
, 0x21
) để hiển thị đoạn text. Tuy nhiên, chương trình có thể viết trực tiếp vào trong video buffer
(dùng cho text mode, nằm ở segment 0x0b000
) vì vậy nó có thể bypass bất cứ OS hay các hàm security nào do 0x9
chỉ định. Cho nên, đa nhiệm (multitasking) là điều không thể bởi vì mỗi ứng dụng có thể viết bất cứ nơi đâu, dẫn đến việc phá hủy các ứng dụng khác hoặc dữ liệu của OS.
còn đây là 1 mẫu "Hello World" cho 16-bit COM:
org 100h ; code starts at offset 100h
use16 ; use 16-bit code
; The SS is same as CS (since only one
; segment is here) and SP is set to 0xFFFE
mov ax,0900h
mov dx,Msg
int 21h;
mov ax,4c00h
int 21h
Msg db "Hello World!$"
có điều gì khác biệt ở đây? tất cả mọi thứ (từ code, data, stack) đều nằm trong một segment. Code bắt buộc bắt đầu từ offset 0x100 (để DOS có thể viết thêm thông tin ở các byte thấp hơn 0x100), vì nằm chung 1 segment nên nó sẽ không có data segment hay stack segment, dẫn đến việc các file COM là một memory map
và được giới hạn 64KB. Vì vậy, COM files hiếm khi được sử dụng.
Tóm lại, 1 chương trình DOS bao gồm 1 số CS(code segment), DS(data segment), và 1 SS(stack segment) như trên. 1 chương trình DOS gọi hàm DOS và BIOS (thông qua interrupt) và hoàn thành công việc của nó.
vì segment bị giới hạn 64KB, sẽ có rất nhiều programming model phụ thuộc vào yêu cầu của application:
- Tiny, khi mọi thứ đều
vừa
trong 1 segment (COM files). - Small, khi có 1 CS và 1 DS, calls và jumps gần nhau.
- Medium, khi có 1 DS nhưng nhiều CS, calls và jumps xa nhau.
- Compact, khi có 1 CS nhưng nhiều DS, calls và jumps gần nhau.
- Large, khi có nhiều CS và DS, calls và jumps xa nhau.
- Huge, khi cấu trúc dữ liệu (data structure) vượt 64KB, vì vậy chúng sẽ tách thành các segment theo 1 thứ tự nhất định.
Model thường gặp nhất là Small và Large
trong code 32-bit protected mode (chúng ta sẽ không bàn đến 16-bit vì nó rất hiếm), 1 segment có thể có bất cứ dung lượng nào, từ 1 byte - 4GB. OS sẽ định dạng kích thước của mỗi segment và lúc này thì sẽ có giới hạn cho mỗi segment (cho phép hoặc không read, write, execute). Điều này cho phép OS có thể "bảo vệ" bộ nhớ. Thêm vào đó, có 4 level chức vụ (từ 0-3, 0 là cao nhất), ví dụ nếu user application chạy ở mức 3, thì không thể nào mà có thể chỉnh sửa được OS application ở mức 0.
Nếu 1 công tác (task) ở 32-bit protected mode bị crash, OS sẽ bắt exception và tắt chương trình một cách an toàn mà không dẫn đến việc crash các application khác hoặc chính bản thân nó - OS. Lúc này, multitasking bắt đầu xuất hiện.
nhiều người nghĩ rằng multitasking là 1 nghệ thuật chạy các application đồng thời cùng lúc. Thực sự không phải vậy, mỗi core của CPU chỉ có thể execute được mỗi command trong 1 thời điểm. Chính xác hơn là OS cho phép Task #1 chạy trong khoảng thời gian X, sau đó chuyển sang Task #2, cũng chạy trong khoảng thời gian X, rồi tới Task #3, ... những việc trên xảy ra quá nhanh khiến cho ta tưởng rằng chúng đồng thời cùng hoạt động.
bật A-20 line là bước đầu để sử dụng bộ nhớ trên 640KB. Thủ thuật này (có thể dùng được ở 286+) là cách để kiếm được 0xfff0 byte của thanh RAM (trong khoảng 0xffff:0x0010 đến 0xffff:0xffff) và có thể truy cập được ở Real mode. để bật line - thông qua bàn phím (tác giả cũng không giải thích tại sao) - buộc CPU tránh việc wrapping. Khoảng bộ nhớ này (được biết đến là High Memory Area, ngắn gọn là HMA) được dùng bởi HIMEM.SYS để nạp các phần của DOS vào trong nó và vì thế nó có nhiều chỗ hơn ở bộ nhớ thấp để cho application dùng.
đoạn code sau thể hiện bật/tắt A20. Chú ý là nếu HIMEM.SYS đã cài, A20 sẽ bật theo mặc định. HIMEM.SYS nên được query
để thay thế trạng thái của A20 thay vì làm nó 1 cách trực tiếp
WaitKBC:
mov cx,0ffffh
A20L:
in al,64h
test al,2
loopnz A20L
ret
ChangeA20:
call WaitKBC
mov al,0d1h
out 64h,al
call WaitKBC
mov al,0dfh ; use 0dfh to enable and 0ddh to disable.
out 60h,al
ret
đoạn code sau sẽ kiểm tra A20 và trả 1 vào CF nếu nó được bật, và 0 khi tắt.
CheckA20:
PUSH ax
PUSH ds
PUSH es
XOR ax,ax
MOV ds,ax
NOT ax
MOV es,ax
MOV ah,[ds:0]
CMP ah,[es:10h]
JNZ A20_ON
CLI
INC ah
MOV [ds:0],ah
CMP [es:10h],ah
PUSHF
DEC ah
MOV [ds:0],ah
STI
POPF
JNZ A20_ON
CLC
POP es
POP ds
POP ax
RET
A20_ON:
STC
POP es
POP ds
POP ax
RET
Global Descriptor Table là bảng bao gồm các segment có thể nhìn thấy được. Mỗi segment sẽ có cơ cấu như:
- kích thước
- base address (địa chỉ vật lí trong bộ nhớ)
- access restriction (giới hạn truy cập)
đối với Protected mode, hệ thống sẽ kiểm soát thanh ghi GDTR
(accessible thông qua SGDT
/LGDT
) gồm 6-byte data:
- 2 bytes của cả chuỗi. bởi vì mỗi GDT entry là 1 8-byte entry, tối đa 8192 entry.
- 4 bytes còn lại là địa chỉ vật lí của chuỗi GDT trong bộ nhớ. ở đây gồm có 2 loại GDT entry, 1 entry dành cho application (S flag == 1, xem bên dưới), và 1 entry dành cho OS (s flag == 0). định nghĩa GDT của một cấu trúc application dựa theo cấu trúc C++ sau:
struct GDT_STR
{
unsigned short seg_length_0_15;
unsigned short base0_15;
unsigned char base16_23;
unsigned char flags;
unsigned char seg_length_16_19:4;
unsigned char access:4;
usigned char base24_31;
};
mặc dù nhìn thì có vẽ đơn giản, nhưng thực chất nó còn phức tạp hơn bạn nghĩ. Bây giờ thử đi qua từng dòng khai báo. Chú ý là đoạn phân tích dưới đây dùng cho bit S được set bằng 1 (user GDT), còn nếu nó bằng 0, thì chúng ta sẽ nói về GDT entry liên quan đến hệ thống.
-
seg_length
- một giá trị 20-bit dùng để miêu tả độ dài của segment. nếu cờ G (xem bên dưới) không được set, thì giá trị này đại diện cho độ dài thực của segment. nếu cờ G được set, giá trị sẽ được nhân 4096 để đại diện cho độ dài của segment. Nên nếu bạn set nó 0xFFFFF (20 bits) và cờ G được set, nó sẽ thành 0x10000 * 4096 = 4GB
-
base
- là giá trị 32 bit chỉ vị trí bắt đầu của segment trên địa chỉ vật lí
-
flag
-
cờ dành cho segment
-
Bit 0: Type
- 0 - Data
- 1 - Code
-
Bit 1: subtype
- dành cho code segment (B0 == 1)
- 0 - Not conforming
- 1 - Confirming. một conforming segment có thể được gọi từ bất cứ segment nào có priviledge bằng hoặc lớn hơn. Cho nên nếu 1 segment được conform với priviledge level 3, bạn có thể gọi tất cả các segment từ level 0 - 2.
- dành cho data segment (B0 == 0)
- 0 - expand up. segment bắt đầu từ base address và kết thúc khi tới giới hạn.
- 1 - expand down. segment bắt đầu ngược lại, với địa chỉ theo chiều ngược. Cờ này được tạo để stack segment để dễ mở rộng, nhưng OS hiện nay không dùng nữa.
- dành cho code segment (B0 == 1)
-
Bit 2: Accessibility
- dành cho code segment (B0 == 1)
chú ý rằng code segment không thể write (unwritable). Tuy nhiên, vì các địa chỉ segment base có thể chồng lên nhau (overlap), bạn có thể tạo một writable code segment giống với địa chỉ base và giới hạn của code segment. - 0 - unreadable (không thể đọc). bất cứ code nào đọc dữ liệu từ segment này sẽ bật exception. - 1 - readable.
- dành cho data segment (B0 == 0)
- 0 - unwritable (không thể ghi). bất cứ code nào ghi dự liệu từ segment này sẽ bật exception. data segment luôn luôn có thể đọc.
- 1 - writable.
-
Bit 3: Access
- 0 - segment không thể truy cập (unaccessible).
- 1 - accessible. CPU sẽ set bit này mỗi khi segment này accessible, cho nên OS có thể biết được tần số sử dụng đến segment này, để nó có thể đệm (cache) vào disk hay không.
-
Bit 4: S
- 0 - descriptor này dùng cho OS. nếu bit này không được set, toàn bộ GDT entry sẽ có ý nghĩa khác nhau
- 1 - descriptor này dùng cho application.
-
Bit 5-6: DPL
- set quyền của segment này, từ 0 (cao nhất) đến 11b (3) (thấp nhất).
-
Bit 7: P
- 1 - thể hiện rằng segment này có trong bộ nhớ. nếu OS cache segment này vào disk, thì set thành 0. bất cứ truy cập nào vào segment đã bị loại bỏ sẽ bật exception. Lúc này OS sẽ cache exception này, reload lại segment vào bộ nhớ, và set bit P lại là 1.
-
-
-
access
-
Bit 0: AVL
- không dùng, set về 0.
-
Bit 1: L
- set 0 cho 32 32-bit segment, nếu set 1 thì nó là 64-bit segment được dùng ở long mode.
-
Bit 2: D
real mode segment luôn mặc định là 16-bit
- khi D không được set (0), opcode mặc định là 16-bit. segment vẫn có thể execute 32-bit command bằng cách thêm 0x66 hoặc 0x67 trước nó (prefix).
- khi D được set, opcode mặc định là 32-bit. segment vẫn có thể excute 16-bit command bằng cách thêm 0x66, 0x67 như trên.
-
Bit 3: G
- Set 1 rồi dùng seg_length * 4096 để tìm độ dài thực của segment như đã nói trên.
-
như các bạn thấy, segment có thể hoàn toàn hiện thị trong bộ nhớ, điều đó cho phép OS cache segment ấy vào disk và reload nó mỗi khi cần.
entry đầu tiên trong GDT luôn là 0. CPU không đọc thông tin từ entry #0 và vì đó nó được cho là dummy
entry. Điều này cho phép người lập trình ghi giá trị 0 vào thanh ghi (DS, ES, FS, GS) mà không bật exception.
đoạn code sau tạo một vài GDT entry, rồi load chúng:
struc GDT_STR s0_15,b0_15,b16_23,flags,access,b24_31
; 'access' taken as a byte, it is actually 4+4 bits
{
.s0_15 dw s0_15
.b0_15 dw b0_15
.b16_23 db b16_23
.flags db flags
.access db access
.b24_31 db b24_31
}
gdt_start dw gdt_size
gdt_ptr dd 0
dummy_descriptor GDT_STR 0,0,0,0,0,0
code32_descriptor GDT_STR 0ffffh,0,0,9ah,0cfh,0 ; 4GB 32-bit code,
9ah = 10011010b = Present, DPL 00,No System,
Code Exec/Read. 0cfh access = 11001111b = Big,32bit,
<resvd 0>,1111 more size
data32_descriptor GDT_STR 0ffffh,0,0,92h,0cfh,0 ; 4GB 32-bit data,
92h = 10010010b = Present, DPL 00, No System, Data Read/Write
stack32_descriptor GDT_STR 0ffffh,0,0,92h,0cfh,0 ; 4GB 32-bit stack
code16_descriptor GDT_STR 0ffffh,0,0,9ah,0,0 ; 64k 16-bit code
data16_descriptor GDT_STR 0ffffh,0,0,92h,0,0 ; 64k 16-bit data
stack16_descriptor GDT_STR 0ffffh,0,0,92h,0,0 ; 64k 16-bit data
gdt_size = $-(dummy_descriptor)
; For each of the descriptors, I create this code.
' I 've only created it now for code32_descriptor.
xor eax,eax
mov ax,CODE32 ; the definition of a CODE32 segment USE32 in our code
shl eax,4 ; make a physical address
mov [ds:code32_descriptor.b0_15],ax ; store the low 16 bytes
shr eax,16
mov [ds:code32_descriptor.b16_23],al ;
mov [ds:code32_descriptor.b24_31],ah ;
; assuming that DS points to the segment which all the above resides
; Set gdt ptr
xor eax,eax
mov ax,ds
shl eax,4 ; By multiplying the segment with 16, we make it a physical address
add ax,dummy_descriptor ; add the offset of the first entry
mov [gdt_ptr],eax ; save pointer to the physical location
mov bx,gdt_start
lgdt [bx] ; load the GDT
chú ý rằng bạn cần tạo entry cho real mode segment nếu bạn muốn access vào dữ liệu trong đó.
Ở real mode, thanh ghi segment (CS, DS, ES, SS, FS, GS) được xem như là một real mode segment. Bạn có thể để bất cứ cái gì trong đó, không quan tâm đến việc nó trỏ đi đâu. bạn có thể read/write/execute từ segment đó. Ở protected mode, các thanh ghi này được nạp với Selector.
Selector
- **Bit 0 - 1: RPL**
- Yêu cầu Protection level. Bắt buộc phải có priviledge bằng hoặc thấp hơn segment DPL.
- **Bit 2: TI**
- Nếu bit này set 1, selector sẽ chọn 1 entry từ LDT thay vì GDT (đọc bên dưới về LDT).
- **Bit 3 - 15:
- Zero based index to the table (GDT or LDT).
Vì vậy, để load ES
cùng với code32 segment, ta sẽ:
mov ax,0008h ; 0-1 : 00 privilege, 2 : 0 (GDT), 3-15 = 1 (Second Entry)
mov es,ax
Trong protected mode, bạn không thể chọn random giá trị cho thanh ghi segment như real mode. Bạn phải cho giá trị có nghĩa nếu không sẽ bật exception.
OS sử dụng LIDT
instruction để load bảng interrupt. IDTR chứa 6-byte dữ liệu, 2 cho độ dài của bảng và 4 cho địa chỉ vật lí.
mỗi entry trong đó bây giờ là 8 byte, miêu tả vị trí của interrupt handler.
struc IDT_STR
{
.ofs0_15 dw ofs0_15
.sel dw sel
.zero db zero
.flags db flags ; 0 P,1-2 DPL, 3-7 index to the GDT
.ofs16_31 dw ofs16_31
}
define one interrupt:
SEGMENT CODE32 USE32
intr00:
; do nothing but return
IRETD
...
SEGMENT DATA16 USE16
idt_PM_start dw idt_size
idt_PM_length dd 0
interrupt0 db 6 dup(0)
idt_size=$-(interruptsall)
...
SEGMENT CODE16 USE16
xor eax,eax
mov eax,CODE32
shl eax,4 ; Make it physical address
add eax,intr00 ; Add the offset
mov [interrupt0 + 2],eax
mov ax,0008h; The selector of our COD32
mov [interrupt0],ax
...
mov bx,idt_PM_start
mov ax,DATA16
mov ds,ax
; = NO DEBUG HERE =
cli
lidt [bx]
Chú ý dòng = NO DEBUG HERE =. Một khi mà bảng IDT đã được reset, real mode debugger sẽ không hoạt động được. Vì vậy nếu bạn cố step vào LIDT
, bạn sẽ bị crash. Và bạn cũng không thể gọi DOS hay BIOS interrupt từ protected mode. Tuy nhiên, Bochs có hardware debugger riêng của nó có thể step vào bất cứ đâu nên bạn có thể làm việc từ chỗ đó!
điều này rất hiếm khi application đầu tiên theo protected mode của bạn không bị crash. Khi điều này xảy ra, CPU sẽ tripple fault và reset. Để tránh việc reset, bạn có thể dùng real code có thể execute được:
MOV ax,40h
MOV es,ax
MOV di,67h
MOV al,8fh
OUT 70h,al
MOV ax,ShutdownProc
STOSW
MOV ax,cs
STOSW
MOV al,0ah
OUT 71h,al
MOV al,8dh
OUT 70h,al
nếu CPU crash, routine này sẽ excute. Routine đó phải reset tất cả thanh ghi và stack, rồi sau đó thoát vào DOS.
cli
mov eax,cr0
or eax,1
mov cr0,eax
Sau đó, bạn phải execute 1 far jump
vào protected mode segment dọn các command đệm bị lỗi (invalid command cache). dùng JMP FAR sẽ bị lỗi, vì lúc này assembler không hiểu rằng là chúng ta đang ở trong protected mode. Nếu là code segment là 16-bit, làm như sau:
db 0eah ; Opcode for far jump
dw StartPM ; Offset to start, 16-bit
dw 018h ; This is the selector for CODE16_DESCRIPTOR,
; assuming that StartPM resides in code16
nếu là 32-bit:
db 66h ; Prefix for 32-bit
db 0eah ; Opcode for far jump
dd StartPM ; Offset to start, 32-bit
dw 08h ; This is the selector for CODE32_DESCRIPTOR,
; assuming that StartPM resides in code32
trước khi bật interrupt, bạn phải step stack và các thanh ghi khác:
mov ax, data_selector
mov ds,ax
mov ax, stack_selector
mov ss,ax
mov esp,1000h ; assuming that the limit of the stack segment
; selected by stack_selector is 1000h bytes.
sti
...
cli
mov eax,cr0
and eax,0ffffffeh
mov cr0,eax
mov ax,data16
mov ds,ax
mov ax,stack16
mov ss,ax
mov sp,1000h ; assuming that stack16 is 1000h bytes in length
mov bx,RealMemoryInterruptTableSavedWithSidt
litd [bx]
sti
; (You can debug here) ...
Vì protected mode không thể gọi DOS hay BIOS interrupt, nên nó thường không hữu dụng lắm cho các application trên DOS. Tuy nhiên, có 1 lỗi trong bộ xử lí 386+ gọi đến 1 mode là unreal mode. Unreal mode là cách dùng để access 4GB bộ nhớ từ real mode. Trick này không có trong sách vở, tuy nhiên rất nhiều các application (bao gồm cả HIMEM.SYS) đều sử dụng nó.
- bật A20.
- có thể vào protected mode.
- nạp thanh ghi segment (ES hoặc FS hoặc GS) cùng với 4GB data segment.
- trở về real mode.
miễn sao thanh ghi không thay đổi giá trị, nó vẫn sẽ trỏ tới 4GB data segment, vì vậy ta có thể sử dụng cùng với EDI
để access vào cả 1 không gian address. Sau khi trở về từ protected mode, bạn có thể:
; assuming FS has loaded a 4GB data segment from Protected Mode
mov edi,1048576 ; point above 1MB
mov byte [fs:edi],0 ; Set a byte above 1MB.
286 thiếu khả năng này vì khi thoát khỏi protected mode, CPU buộc phải reset, nên mọi thanh ghi đều biến mất.
hàm này là một routine sẽ đặt CPU bạn ở real mode và set FS thành 32-bit data segment:
struc GDT_STR
s0_15 dw ?
b0_15 dw ?
b16_23 db ?
flags db ?
access db ?
b24_31 db ?
ENDS
SEGMENT CODE16 USE16 PUBLIC
ASSUME CS:CODE16
; GDT definitions
gdt_start dw gdt_size
gdt_ptr dd 0
dummy_descriptor GDT_STR <0,0,0,0,0,0>
code16_descriptor GDT_STR <0ffffh,0,0,9ah,0,0> ; 64k 16-bit code
data32_descriptor GDT_STR <0ffffh,0,0,92h,0cfh,0> ; 4GB 32-bit data, 92h = 10010010b = Presetn , DPL 00, No System, Data Read/Write
gdt_size = $-(dummy_descriptor)
dummy_idx = 0h ; dummy selector
code16_idx = 08h ; offset of 16-bit code segment in GDT
data32_idx = 10h ; offset of 32-bit data segment in GDT
PUBLIC _EnterUnreal
PROC _EnterUnreal FAR
PUSHAD
PUSH DS
PUSH CS
POP DS
mov ax,CODE16 ; get 16-bit code segment into AX
shl eax,4 ; make a physical address
mov [ds:code16_descriptor.b0_15],ax ; store it in the dscr
shr eax,8
mov [ds:code16_descriptor.b16_23],ah
XOR eax,eax
mov [ds:data32_descriptor.b0_15],ax ; store it in the dscr
mov [ds:data32_descriptor.b16_23],ah
; Set gdt ptr
xor eax,eax
mov ax,CODE16
shl eax,4
add ax,offset dummy_descriptor
mov [gdt_ptr],eax
cli
mov bx,offset gdt_start
lgdt [bx]
mov eax,cr0
or al,1
mov cr0,eax
mov ax,data32_idx
mov fs,ax
mov eax,cr0
and al,not 1
mov cr0,eax
MOV AX,0
MOV FS,AX
POP DS
POPAD
RETF
ENDP
Global Descriptor Table
khi cờ S
được set về 0, thì GDT sẽ có ý nghĩa khác
flags - Flags dùng cho segment more on gate later in this article
- Bits 3 2 1 0: Type of entry
- 0000 - dự phòng
- 0001 - được dùng cho 16-bit TSS
- 0010 - Local Descriptor Table (LDT)
- 0011 - Busy 16-bit TSS
- 0100 - 16-bit call gate
- 0101 - Task gate
- 0110 - 16-bit interrupt gate
- 0111 - 16-bit trap gate
- 1000 - dự phòng
- 1001 - được dùng cho 32-bit TSS
- 1010 - dự phòng
- 1011 - Busy 32-bit TSS
- 1100 - 32-bit call gate
- 1101 - dự phòng
- 1110 - 32-bit interrupt gate
- 1111 - 32-bit trap gate
Local Descriptor Table (LDT) là phương pháp mà mỗi application dùng để tạo một set segment riêng, nạp với LIDT
instruction. LDT ở selector sẽ được định danh nếu segment được nạp từ GDT hoặc LDT. Mặc dù điều này hữu ích, nhưng nó không được dùng ở các OS hiện đại vì Paging.
call gates là cơ chế để chuyển từ priviledge code thấp đến mức cao hơn, used for user-level code to call system-level code
. Bạn có thể định danh 1100 loại entry trong GDT theo format sau:
struct CALLGATE
{
unsigned short offs0_15;
unsigned short selector;
unsinged short argnum:5; // number of arguments to copy to the stack from the current stack
unsigned char r:3; // Reserved
unsigned char type:5; // 1100
unsigned char dpl:2; // DPL of this gate
unsigned char P:1; // Present bit
unsigned short offs16_31;
};
Dùng CALL FAR cùng với selector của callgate này (không quan tâm offset) sẽ đổi gate và execute command ở priviledge cao hơn. If argnum specifies parameters to be copied, the system copies them to the new stack after pushing SS,ESP,CS,EIP. Using RETF will return from the gate call.
bởi vì hiện nay có lệnh SYSENTER/SYSEXIT nhanh hơn, gates đã không còn được sử dụng nữa. Chức năng của nó đã bị giới hạn lại còn:
- khi bạn cần chuyển đổi giữa Ring 3 <-> Ring 0 (sys command chỉ thay đổi từ 3 - 0 và ngược lại).
- khi exploit malware, patch GDT để gọi gates và execute priviledge commands. Chú ý ở x64 Windows, "Kernel Patching Protection" sẽ chặn việc thay đổi GDT.
các instruction này là cách mà để thay đổi giữa ring 3 và 0. Bạn sẽ dùng WRMSR để set giá trị mới cho CS (0x174), XSP(0x175), và XIP(0x176). XCX giữ giá trị ring 3 của SP cho SYSEXIT và XDX giữ giá trị ring 3 của IP cho SYSEXIT. Entry chứa CS phải index tới 4 selector, đầu tiên là ring 0 code, thứ 2 là ring 0 data, thứ 3 là ring 3 code và thứ tư là ring 4 data. Những giá trị này cố định nên để có thể dùng SYSENTER và SYSEXIT thì GDT của bạn phải chứa các entry này trong format của bạn.
opcode này chỉ hỗ trợ từ chuyển đổi từ ring 3 và ring 0, nhưng mà chúng nhanh hơn
Sẽ có nhiều vấn đề xảy ra khi Multitasking cùng với cách setup trên:
- Mỗi task buộc phải được nạp vào bộ nhớ đầy đủ.
- DOS application bản thân chúng luôn cho rằng mình sẽ access vào số MB thấp nhất trong RAM, cho nên mình không thể bỏ nó vào chỗ khác.
- Mỗi application phải có khả năng tự giải quyết segment của chính nó (điều này hoàn toàn khác với các application khác), vì vậy việc tạo một app được link tới một thư viện tĩnh (static library) cực kì tốn chi phí.
Paging là phương pháp dùng để chuyển hướng (redirect) địa chỉ này đến địa chỉ khác. Địa chỉ mà app dùng được gọi là "linear address" và địa chỉ thực là "physical address".
32 bit Protected Mode Paging
Hình thái đơn giản của paging bao gồm 2 bảng: Page Directory và Page Table. Page Directory là một chuổi 1024 32-bit entry với format như sau:
PRUWDANSGA-Addr
- P - chỉ page hiện tại trong bộ nhớ. Cờ này cho phép OS cache các page vào disk, sau đó xóa giá trị P, rồi nạp lại lúc page fault được tạo ra phần mềm cố gắng access vào page.
- R - có thể read/write nếu được set, còn không chỉ được read.
- U - nếu được set, chỉ có ring 0 được access vào page này.
- W - nếu set, write-through được bật.
- D - nếu set, page sẽ không được cache.
- A - set khi page bị access (không tự động, khá giống với bit GDT).
- N - set về 0.
- S - set về 0. nếu PSE được bật, xem bên dưới.
- G - set về 0.
- Addr - The upper 20 bits (the lower 12 are ignored because it must be 4096- aligned) of the Page Table entry that this Page Directory entry points to.
Page Table là 1 chuỗi 1024 32-bit entry cùng giống format trên. The address points to the actual physical address that this page is mapped to.
vì ta có 1024 Page Directory và 1024 Page Table, ta có tổng cộng 1024x1024 cách mapping nó. Vì kích thước của page là 4096 bytes, ta có thể map tất cả địa chỉ 32 bit.
đặt CR3 vào địa chỉ của page đầu tiên trong Page Directory entry trước khi bật Paging (CR0 PE bit).
PSE
Nếu PSE được bật (CR4 bit 4), thì S có thể là 1, lúc này thì kích thước page là 4MB, và page phải được xếp theo 4MB đó. Mode này dùng để tránh việc tách nhỏ thành các page(tưởng tượng 1000 page xem), dẫn đến việc lãng phí bộ nhớ khi ta cần dùng lớn hơn 4MB. May mắn thay, ta có thể dùng nhiều mode kết hợp.
Physical Address Extension (PAE)
PAE là khả năng của x86 dùng 36 địa chỉ bit thay vì 32. Điều này nó tăng lượng bộ nhớ dùng được từ 4GB thành 64GB. 32-bit app vẫn chỉ thấy được 4GB không gian địa chỉ, nhưng OS có thể map (thông qua paging) bộ nhớ từ địa chỉ cao đến thấp của 4GB. Extension này được thêm vào x86 để có thể hỗ trợ với sức chứa của 4GB (nhưng hiện nay thì không đủ), cho đến khi x64 bước vào cuộc chơi.
Bật PAE (CR4 bit 5) nghĩa là bạn có 3 level để paging: ngoài việc có thêm PT và PDT, bây giờ bạn có PDTD, Page Directory Pointer Table, có 4 64-bit entry. Mỗi PDTD entry sẽ trỏ đến 4KB Page Directory (như paging thông thường). Mỗi entry trong Page Directory mới bây giờ dài đến 64 bit (cho nên mình có 512 entry): mỗi entry trong Page Directory mới trỏ vào 4 KB Page Table (như paging thông thường), và mỗi entry trong Page Table bây giờ là dài 64 bit, và cho nên nó có 512 entry. Vì nó chỉ cho phép dùng 1/4 so với mapping ban đầu, cho nên vì sao 4 Directory/Table được hỗ trợ. Entry đầu tiên map 1GB đầu tiên, entry thứ 2 map 1GB thứ 2, cho đến cuối cùng là entry thứ 4 map 1GB thứ 4.
Nhưng bây giờ bit S trong PDT mang ý nghĩa khác: nếu không được set, có nghĩa là page entry là 4KB nhưng nếu được set, có nghĩa là entry này không trỏ vào PT entry, nhưng nó miêu tả bản thân là 1 page 2MB. Vì vậy bạn sẽ có nhiều level paging tùy thuộc vào bit S.
Ở đây cũng sẽ có cờ mới trong Page Directory entry, bit NX(bit 63), nếu được set nó sẽ ngăn code execute trong page đó.
Hệ thống này cho phép OS quản lí bộ nhớ lớn hơn 4GB, vì address space vẫn là 4GB, nên mỗi process vẫn bị giới hạn tại 4GB. Bộ nhớ có thể lên đến 64GB nhưng 1 process không thể thấy được điều đó.
trước đó thì LDT được dùng để giúp cho việc tạo các chuỗi cục bộ cho các segment. Nhưng vì có paging, 32-bit OS hiện đại bây giờ dùng flat mode
. Cách này thì app sẽ nhận cả 4GB address space để chứa code, data, stack nhưng phần này được map vào một địa chỉ vật lí hoafn toàn khác. Vì vậy app có thể sử dụng cùng một địa chỉ bộ nhớ trong khi nó được map vào mỗi địa chỉ vật lí khác nhau.
Ví dụ, có 2 chương trình C++ chạy trên Windows 32-bit:
int main()
{
; CS:EIP at this point is, (say) 010Ch : 00004000h.
int flags = MB_OK;
char* msg = "Hello there";
char* title = "Title";
MessageBox(0,msg,title,flags); ; Address of message box is (say) 00547D45h
}
int main()
{
; CS:EIP at this point are the same as in previous program. However paging actually
; maps them to a different physical address so these
; two programs do not interfere with same memory.
; This is transparent to the application
int flags = MB_OK;
char* msg = "Hello there";
char* title = "Title";
MessageBox(0,msg,title,flags);
; Address of message box is (say) 00547D45h,
; and this value is mapped to the same
; memory as in the previous application, so the shared function
; "MessageBox" is only once found in physical memory.
}
Điều này cho phép lập trình viên không cần quan tâm đến segmentation là gì. mọi con trỏ đều ở gần, không hề có segment (tất cả đều cùng giá trị), nên dẫn đến việc tạo app đơn giản hơn. Và ở đây cũng không hề có "small/large" model, bởi vì tất cả đều cùng nằm trong 1 segment.
The OS maps via paging all needed memory (say, a DLL) to some virtual address in the 32-bit address space, and the app will never consider far pointers or segmentation
. CS, DS và SS được mặc định xem như là 4GB address space, nhưng tất cả địa chỉ đều là ảo và được map vào app thông qua Paging, vì vậy sẽ không có segmentation.
Vì vậy, trong Flat mode:
- Tất cả segment đều được mở rộng thành 32-bit 4GB
- Thông qua Paging, các địa chỉ linear khác nhau được map vào cùng 1 địa chỉ vật lí giống nhau và các địa chỉ linear giống nhau được map vào các địa chỉ vật lí khác nhau
- không có segmentation, LDT, call gates. Không có ring 1, 2. ENTER thông qua SYSENTER/SYSEXIT (which btw share now the old LOADALL opcodes).
Bởi vì sự đơn giản của flat mode, hiện nay nó đều được dùng bởi tất cả các OS 32-bit và cũng là mode duy nhất tồn tại trong 64 bit.
So far all nice with protected mode, but many of the existing applications were real-mode at that time. Even today, many (mostly games) are played under Windows. To force these applications (which think they own the machine) to cooperate, a special mode should be created.
VM86 mode là một cờ đặc biệt trong thanh ghi**EFLAG
**, cho phép bộ nhớ 640KB 16-bit DOS được forward thông qua Paging đến địa chỉ thực - điều này giúp cho việc chạy nhiều DOS app cùng lúc mà không bị overwrite với nhau. EMM386.EXE, ngày xưa thường được gọi mà memory manager, giúp cho processor thực hiện điều đó. OS thực hiện từng bước quan sát quá trình đó để chắc chắn rằng nó sẽ không execute bất cứ thứ gì điên rồ (vì vậy đừng mong chờ nó sẽ tự vào protected mode khi EMM386.EXE được load bởi vì một khi bạn set GDT bằng LGDT
, you screwed).
Khi cờ VM được set, bạn có thể load một "segment" thông thường vào thanh ghi segment. interrupts call bởi DOS app được bắt bởi OS và emulate
nó - nếu có thể. Cùng với đó là một vài instruction bị bỏ qua, ví dụ, nếu bạn CLI, interrupt thực ra không phải bị vô hiệu hóa. OS thấy rằng bạn không cần interrupt và làm việc tiếp tục, nhưng interrupt vẫn còn đó.
tất cả VM86 code excute ở PL 3, priviledge thấp nhất. cổng Ins/Outs cũng được bắt và emulate nếu có thể. Điều thú vị về VM86 là nó có 2 bảng interrupt, one for the real and one for the protected mode. But only protected mode interrupts are executed
.
VM86 không còn trên 64-bit, nên 64-bit OS không thể execute được 16-bit DOS code. để execute được bạn cần một emulator như là DosBox.
HIMEM là một trình quản lí bộ nhớ mở rộng cho DOS. vào thời điểm đó, bộ nhớ mở rộng thường được dùng để cache data từ disk, đặc biệt là từ các app lớn. HIMEM đặt CPU ở real mode (hoặc dùng LOADALL ở 286) và cung cấp một giao diện đơn giản. Bằng việc bật A20 line, HIMEM cho phép 1 phần của DOS COMMAND.COM nằm ở địa chỉ cao. Bởi vì real mode vẫn là real mode, nên protected mode app có thể làm việc mà ta đã nói trên cho dù HIMEM.SYS được load.
Vào thời điểm đó, bộ nhớ mở rộng (đã bị loại bỏ trước đó) được hình thành. Có khá nhiều app lợi dụng điều đó, nhưng tiêu chuẩn ở hiện đại là protected mode. EMM386 đặt CPU ở VM86 mode và map thông qua Paging bộ nhớ hơn 1MB segment của real mode (0xA0000), cho nên nếu một app cần dùng bộ nhớ mở rộng có thể dùng nó thông qua EM386.EXE. In addition, EMM386 allowed "devicehigh" and "loadhigh" commands in CONFIG.SYS, allowing applications to get loaded to these high segments if possible.
DOS Protected Mode Interface là một hệ thống cho phép DOS app chạy 32-bit code. Unreal mode là không đủ vì nó chỉ có thể di chuyển data, nhưng không thể execute. Điều mà DPMI server có thể làm là giải quyết các loại table đã nói trên, cho phép executable định danh 32-bit code trực tiếp. Khi executable gọi DOS, DPMI server bắt call đó, đổi nó sang real mode, và trở về protected mode.
At that time, a now non-existent and mostly undocumented instruction existed, LOADALL (0xF 0x5 in 286, 0xF 0x7 in 386). LOADALL, như tên của nó, dùng để load tất cả register (bao gồm GDTR và IDTR) từ 1 table trong bộ nhớ. Trong 286 LOADCALL (không access được từ 386), có địa chỉ bộ nhớ mặc định là 0x800, trong khi 386 LOADALL đọc buffer được trỏ bởi real mode ES:EDI. Bởi vì CPU không có cách nào để biết được bất cứ giá trị nào load bởi LOADALL có nghĩa, LOADALL được dùng bởi rất nhiều tool tại thời điểm đó, bao gồm HIMEM.SYS, cho nhiều mục đích:
- Để access toàn bộ memory từ real mode mà không cần thông qua protected mode và unreal mode.
- Để chạy code thực thông qua Paging.
- Để trở về code thực từ protected mode mà không cần reset 286.
- Để chạy 16-bit code thông thường trông protected mode mà không cần VM86. Điều này được thực hiện bằng cách trap mỗi memory address (sẽ dẫn đến GDF bởi tất cả segment được đánh dấu là non-present) và emulate kết quả bằng việc dùng một LOADALL khác. Tất nhiên là điều này rất chậm, nó dẫn đến việc hình thành VM86 mode ở 386, nơi mà LOADALL không dùng được.
LOADALL 286 được nhắc đến 1 phần trong tài liệu và ngược lại, 386 thì không có 1 thông tin gì.
x64 CPU có 3 models:
- Real mode, như DOS
- Legacy mode, như 32-bit protected mode
- Long mode
long mode có 2 sub-mode:
- Compatibility mode: như 32-bit protected mode. Điều này cho phép 64-bit OS chạy 32-bit app.
- 64-bit mode: cho 64-bit app
để hoạt động trên long mode, programmer phải chú ý những điều sau:
- không giống như protected mode, có thể chạy có hoặc không có Paging, long mode bắt buộc cần PAE và paging. That is, you cannot leave paging out even if your map is "see-through". Bạn phải tạo 1 PAE - style page tables và "flat" mode là mode duy nhất dùng được trong long mode. Không có segmentation.
- Tài liệu AMD nói rằng, để vào long mode, bạn phải vào protected mode - tuy nhiên, điều này được chứng minh là không đúng, vì bạn có thể vào long mode trực tiếp từ real mode, bằng cách bật protected mode và long mode bằng 1 instruction (điều này có thể thực hiện được vì control register access được ở real mode).
- Mặc dù theo lí thuyết là bất cứ 64-bit giá trị có thể dùng như 1 địa chỉ, thực tế thì chúng ta còn chưa dùng tới 2^64 bộ nhớ.
Therefore, current implementations only implement 48-bit addressing, which enforces all pointers to have bits 47-63 either all 0 or all 1. This means that you have 2 ranges of valid "canonical" addresses, one from 0 to 0x00007FFF'FFFFFFFF and one from 0xFFFF8000'00000000 through 0xFFFFFFFF'FFFFFFFF, for a 256TB of total space
. Đa số OS thường dự phòng upper area cho kernel, và lower area cho user.And no, you cannot use the useless bits to store extra smart information about the pointer, because the CPU does not ignore these bits, it enforces them to be either all 1 or all 0. Forget your bad habits.
trong long mode sẽ thêm vào một cấu trúc top level mới, PML4T có 512 64-bit entry trỏ vào PDPT và bây giờ PDPT có 512 entry nghĩa là mỗi PT entry quản lí 4KB, 1 PDT entry quản lí 2MB (4KB*512 entry), 1 PDPT entry quản lí 1GB (2MB*512 PDT entry), và 1 PML4T quản lí 512GB(1GB*512 PDPT entry). vì ta có 512 PML4T entry, nên giờ ta có tổng cộng 256TB (512GB*512 PML4T entry) có thể dùng được.
ta có lí do khác để không dùng toàn bộ 64-bit địa chỉ. Vì nếu dùng toàn bộ ta buộc phải có đến 6 level paging.
Mỗi bit S trong PDPT/PDT có thể là 0 để chỉ ra rằng là ta có 1 cấu trúc lower level bên dưới, hoặc 1 là ngược lại. nếu bit S của cờ PDPT set 1, thì kích cỡ page là 1GB.
-
Tạo 64-bit segment
- 1 segment được đánh dấu là 64-bit khá giống với 32-bit với giới hạn 4GB, nhưng với bit
L
được set 1 và bitD
set 0. Bit D set 0 ở 16-bit segment, nhưng khi bit L được set, nó sẽ thành 64-bit segment.
- 1 segment được đánh dấu là 64-bit khá giống với 32-bit với giới hạn 4GB, nhưng với bit
-
64-bit segment luôn bắt đầu từ 0 - 0xFFFFFFFFFFFFFFFF
nếu GDT của bạn nằm ở 4GB thấp, bạn không cần thay đổi nó khi vào long mode. Tuy nhiên, nếu bạn tính gọi SGDT
hay LGDT
khi đang ở long mode, bạn phải giải quyết với 10 byte GDTR, vì nó giữ 2 byte cho độ dài của GDT và 8 byte của địa chỉ vật lí.
Bất cứ selector nào bạn nạp vào để access 64-bit segment đều bị bỏ qua, và DS
, ES
, SS
không được dùng. Tất cả các segment đều phẳng và mọi thứ đều được hoàn thành thông qua paging. Kết thúc thời kì của segmentation.
Bạn phải reset IDT để dùng 64-bit descriptor
mỗi entry trong nó lúc này là 16 byte, nó miêu tả vị trí của interrupt handler ở 64-bit mode.
struc IDT_STR
{
.ofs0_15 dw ofs0_15
.sel dw sel
.flags db flags
.ofs16_31 dw ofs16_31
.ofs32_63 dd ofs32_63
.zero dd zero
}
; Disable paging, assuming that we are in a see-through.
mov eax, cr0 ; Read CR0.
and eax,7FFFFFFFh; Set PE=0
mov cr0, eax ; Write CR0.
mov eax, cr4
bts eax, 5
mov cr4, eax
mov ecx, 0c0000080h ; EFER MSR number.
rdmsr ; Read EFER.
bts eax, 8 ; Set LME=1.
wrmsr ; Write EFER.
; Enable Paging to activate Long Mode. Assuming that CR3
' is loaded with the physical address of the page table.
mov eax, cr0 ; Read CR0.
or eax,80000000h ; Set PE=1.
mov cr0, eax ; Write CR0.
- Tắt paging nếu nó được bật. Để làm điều đó, bạn phải chắc chắn rằng, bạn phải đang chạy ở "see through" area.
- set PAE bằng cách set bit thứ 5 của CR4
- Tạo page table mới và load
CR3
vào nó. bởi vìCR3
vẫn ở 32-bit trước khi vào long mode, nên page table vẫn ở 4GB thấp. - Bật long mode (chú ý rằng đây chỉ là bật, chứ không đi vào long mode)
- Bật paging và vào long mode.
bởi vì rdmsr/wrmsr
opcode có thể dùng ở real mode, bạn có thể kích hoạt long mode từ real mode trực tiếp bằng cách set đồng thời PE và PM bit của CR0.
Bây giờ bạn đang ở Compatibility mode. Vào 64-bit mode bằng cách jump 64-bit code segment:
; also db 066h if entering from a 16-bit code segment
db 0eah
dd LinearAddressOfStart64
64-bit segment đầu tiên phải nằm ở 4GB thấp bởi vì Compatibility mode không thể thấy địa chỉ 64-bit.
Chú ý rằng bạn phải dùng địa chỉ linear, bởi vì 64-bit segment luôn bắt đầu từ 0. Chú ý rằng nếu Compatibility segment hiện tại mặc định là 16-bit, bạn phải dùng 0x66 bit prefix.
điều duy nhất bạn phải làm ở 64-bit mode là reset RSP
:
linear rsp,stack64_end
linear là 1 macro tìm linear address của 1 target. SS
, DS
, ES
, không được dùng ở 64-bit mode. nghĩa là nếu bạn muốn access data ở 1 segment khác, bạn không thể load DS cùng với segment selector ở trong đó và access vào data. Bạn phải nêu ra linear address của data đó. Data và stack luôn được access với linear address. Flat mode không những mặc định, mà còn là duy nhất. Tuy nhiên GS và FS vẫn có thể dùng như một auxiliary register và giá trị của chúng vẫn subject to verification from the GDT
. ở Windows, FS trỏ đến Thread Information Block
khi bạn ở trong 64-bit mode, mặc định cho opcode (trừ jmp
/call
) vẫn ở 32-bit. Vì REX prefix được yêu cầu (0x40 - 0x4F) để đánh dấu 64-bit code. Assembler sẽ tự động giải quyết nếu nó là "code64" segment.
thêm vào đó, bảng 64-bit interrupt bây giờ phải set với LIDT
instruction mới, lấy 10 byte operator (2 byte cho độ dài và 8 byte cho vị trí), và mỗi entry trong bảng IDT chiếm 10 byte, 2 byte cho selector và 8 byte cho offset.
Bởi vì 0xEA không phải là jump hợp lệ ở 64-bit mode, bạn phải dùng RETF
trick để trở về Compatibility mode segment
push code32_idx ; The selector of the compatibility code segment
xor rcx,rcx
mov ecx,Back32 ; The address must be an 64-bit address,
; so upper 32-bits of RCX are zero.
push rcx
retf
code trên sẽ giúp bạn trở về compatibility mode. 64-bit OS sẽ nhảy liên tục từ 64-bit đến compatibility mode để có thể chạy cả 64 và 32 bit app.
tại sao Windows driver phải 64-bit cho 64-bit OS? bởi vì code WOW64 cho driver (ring 0) tồn tại. chúng có thể tạo ra 1 cái nếu muốn - tác giả đoán rằng chúng muốn buộc các cơ sở hạ tầng phải hoạt động 64-bit.
Bạn lại phải setup tất cả các register với 32-bit selector - trở về với segmentation. Bạn cũng phải ở see-through area bởi vì để thoát long mode bạn phải tắt paging. và dĩ nhiên, thay vì tắt bạn có thể đổi thành real mode bằng cách reset PM bit.
; We are now in Compatibility mode again
mov ax,stack32_idx
mov ss,ax
mov esp,stack32_end
mov ax,data32_idx
mov ds,ax
mov es,ax
mov ax,data16_idx
mov gs,ax
mov fs,ax
; Disable Paging to get out of Long Mode
mov eax, cr0 ; Read CR0.
and eax,7fffffffh ; Set PE=0.
mov cr0, eax ; Write CR0.
; Deactivate Long Mode
mov ecx, 0c0000080h ; EFER MSR number.
rdmsr ; Read EFER.
btc eax, 8 ; Set LME=0.
wrmsr ; Write EFER.
; Back to the dirty, old, protected mode :(
phần này có vẻ ý tác giả muốn nói là không có unreal mode ở 64-bit
khi CPU vào Long mode, VM86 không được hỗ trợ nữa. đó chính là lí do vì sao 64-bit os không thể chạy 16-bit app. tuy nhiên emulator như DosBox có thể chạy game 16-bit cũ.
không tồn tại, nhưng tác giả có nói về điều tương tự, nó cho phép DOS app chạy đa thread trong real, protected và long mode khi vẫn giữ access tới DOS interrupt http://www.codeproject.com/Articles/894522/Teh-Low-Level-M-ss-DOS-Multicore-Mode-Interface