The 80286 Call Gate Descriptor
In addition to segment descriptors used to restrict memory access, the 80286 also has gate descriptors used to restrict transfers of program control.
The 80286 has four kinds of gate descriptors: call gate descriptors, interrupt gate descriptors, trap gate descriptors, and task gate descriptors.
These descriptors are also stored in the GDT, LDT, and IDT descriptor tables, and are distinguished from segment descriptors by the Type field. Call gate descriptors can be stored in the GDT and LDT, while interrupt gate descriptors and trap gate descriptors can be stored in the IDT.
Making System Calls Through a Call Gate
Put simply and somewhat imprecisely, call gate descriptors are mainly used to implement system calls. They have now essentially been widely abandoned by operating systems (in favor of the sys_enter and sys_exit instructions). Nevertheless, we still need to learn their basic principles.
The x86 CPU has four privilege levels, from ring0 through ring3. ring0 has the highest privilege, while ring3 has the lowest. The Linux operating system uses only ring0 and ring3, which run kernel-mode and user-mode code, respectively. The Linux kernel contains many functions, but only about 200 of them can be called directly by user-mode programs. These functions are called system calls. If users could call non-public operating-system functions, they would pose a threat to the operating system.
From a hardware perspective, how can user-mode programs be allowed to call some operating-system functions (system calls) but not others?
After learning about segment descriptors, we might think of using them. We could place the code for system functions that users should not call in a code segment with a DPL of 0, and place the system functions that users are allowed to call in a code segment with a DPL of 3. This would ensure that users could access only operating-system code in code segments with a DPL of 3.
This looks quite good, as though we have already solved the problem. In fact, this approach has problems:
- Suppose a system call is someProc and we place it in code with DPL 3, allowing users to call it freely. Merely calling it is not a problem. The problem is that a user can freely jmp to the code at someProc+offset and continue execution there, which makes things extremely convenient for anyone intent on causing damage.
- If the segment descriptor for a system-call function has a DPL of ring3, how can user code switch to ring0 when calling it and thereby gain permission to call other operating-system functions?
Therefore, this approach does not work. This is where the call gate is introduced, while the segment descriptor for the system call code still has a DPL of ring0.
A call gate allows the operating system to create a call gate descriptor for each function that it wants user programs to call. When users call operating-system code, rather than directly executing call someProc, they call the selector of this call gate descriptor. Calling a function through a call gate descriptor selector follows these rules:
- someProcGateDescriptorSelector is the selector of the call gate descriptor, and this selector must be loaded into the CS register.
- Unlike an ordinary function call, this kind of call does not refer to the offset in the IP register at all. The CPU locates the gate descriptor directly through its selector.
- The gate descriptor records the segment selector and the offset within the segment for the corresponding operating-system function, as well as how many parameters the function has.
- Using the segment selector in the gate descriptor, the CPU locates the descriptor for the code segment containing the function, thereby determining the location of that code segment. It then adds the offset in the gate descriptor to obtain the exact address of the function and completes the call.
Because this process does not refer to the value in the IP register, a user program cannot arbitrarily jump to any location in the operating-system code and begin executing there.


The rules now become the following:
- The segment descriptors for all operating-system code have a DPL of 0, allowing that code to call one another. The segment descriptors for all user code have a DPL of 1, likewise allowing that code to call one another.
- The APIs that the operating system exposes externally are called system calls. The C bit in the segment descriptor for a system-call code segment is set to 1, making it a conforming code segment. The C bit in the descriptors for all other code segments is 0, making them nonconforming code segments. Code in nonconforming code segments may be accessed only by code at the same privilege level; cross-privilege access is strictly prohibited. In other words, user-mode code may access only user-mode code, and kernel code may access only kernel code. Code in a conforming code segment permits user-mode code to access kernel code, but does not permit kernel-mode code to access user-mode code. That is, a higher privilege level (with a smaller ring number) is not allowed to access code at a lower privilege level (with a larger ring number).
Code Examples of Calls Through a Call Gate and Conventional Function Calls
Conventional Intersegment Function Call
An example of a function call across code segments is as follows:
; caller代码段
code1 segment
....
call someProc
....
code1 ends
;callee 代码段
code2 segment
....
someProc PROC
...
RET
someProc ENDP
code2 endsIntersegment Function Call Through a Call Gate
[SECTION .gdt] ;定义GDT
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32; 非一致代码段,32
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段,16
; **目标代码段的段描述符**
LABEL_DESC_CODE_DEST: Descriptor 0,SegCodeDestLen-1, DA_C+DA_32; 非一致代码段,32
LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA+DA_32;Stack, 32 位
LABEL_DESC_LDT: Descriptor 0, LDTLen-1, DA_LDT ; LDT
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; **调用门描述符**
LABEL_CALL_GATE_TEST: Gate SelectorCodeDest, 0, 0, DA_386CGate+DA_DPL0
......
;**目标代码段选择子**
SelectorCodeDest equ LABEL_DESC_CODE_DEST - LABEL_GDT
;**门描述符选择子**
SelectorCallGateTest equ LABEL_CALL_GATE_TEST - LABEL_GDT Caller code segment:
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
......
call SelectorCallGateTest:0
; END of [SECTION .s32]Callee code segment:
[SECTION .sdest]
[BITS 32]
LABEL_SEG_CODE_DEST:
;jmp $
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 12 + 0) * 2 ; 屏幕第 12 行, 第 0 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'C'
mov [gs:edi], ax
retf
SegCodeDestLen equ $ - LABEL_SEG_CODE_DEST
; END of [SECTION .sdest]Summary
Ordinary Transfers (Without Going Through a Gate)
A JMP or Call followed by a 48-bit full pointer (a 16-bit segment selector plus a 32-bit address offset), where the segment selector points to a code-segment descriptor, is called a direct (ordinary) transfer. An ordinary transfer cannot change the privilege level; that is, it does not cause CPL to change. The details are as follows:
The target is a conforming code segment:
Requirement: CPL >= DPL; RPL is not checked.
CPL after the transfer = CPL before the transferThe target is a nonconforming code segment:
Requirement: CPL = DPL AND RPL<= DPL
CPL after the transfer = CPL before the transferA Transfer Through a Call Gate
When the target segment selector following an intersegment JMP or intersegment CALL instruction points to a call gate descriptor, the transfer uses a call gate. If the selector is followed by a 32-bit address offset, the CPU will not use it, because the call gate descriptor already records the target code’s offset. A transfer through a call gate involves one more step than an ordinary transfer: when the call gate descriptor is accessed, its access permissions must be checked as though the descriptor were a data segment. The RPL of the selector that identifies the call gate must be less than or equal to the gate descriptor’s DPL, and the current code segment’s CPL must also be less than or equal to the gate descriptor’s DPL. This is just like accessing a data segment, where the CPL of the program accessing the data segment must be less than or equal to the DPL of the data segment being accessed, and the selector’s RPL must be less than or equal to the DPL of the data segment or stack segment being accessed. Only when these conditions are met will the CPU proceed to read the target code segment’s selector and address offset from the call gate descriptor and perform the next operation.
After the target code’s segment selector and address offset have been read from the call gate, the information currently available is once again the same as before, placing us at the same starting point as an ordinary transfer (an ordinary transfer has the target code’s segment selector and address offset from the outset). The difference is that the CPU now clears the RPL in the target code segment selector it has read to 0, thereby ignoring the role of the RPL in the code-segment selector stored in the call gate. After this step, the CPU begins privilege-level checks on the current program’s CPL, the RPL of the target code segment selector (which in fact will always satisfy the requirement after being cleared to 0), and the DPL in the target code segment descriptor identified by the target code selector. It then performs the transfer according to the situation, as detailed below:
The target is a conforming code segment:
Requirement: CPL >= DPL; RPL is not checked. Because RPL is cleared to 0, RPL <= DPL is in fact always satisfied. This is the same as for an ordinary transfer and applies to both JMP and CALL.
CPL after the transfer = CPL before the transfer, so no privilege-level transition occurs.The target is a nonconforming code segment:
When the transfer uses a JMP instruction:
Requirement: CPL = DPL (RPL is cleared to 0 and is not checked). If this requirement is not met, the program causes an exception.
CPL after the transfer = DPL
Because the prerequisite is CPL=DPL, setting the program's CPL to DPL after the transfer does not change CPL, and no privilege-level transition occurs. If the prerequisite CPL=DPL is not met when access is attempted, an exception is raised.When the transfer uses a CALL instruction:
Requirement: CPL >= DPL (RPL is cleared to 0 and is not checked). If this requirement is not met, the program causes an exception.
CPL after the transfer = DPL
When CPL=DPL, CPL=DPL after the program transfers, so no privilege-level transition occurs. When CPL>DPL, CPL=DPL after the program transfers, so a privilege-level transition occurs. This is the only transfer method we have seen so far that changes the program's current execution privilege level (CPL): a transfer using a CALL instruction plus a call gate, with a nonconforming code segment as the target.Appendix
Gate Descriptor Data Structure
typedef struct _CALL_GATE
{
USHORT OffsetLow;
USHORT Selector;
UCHAR NumberOfArguments:5;
UCHAR Reserved:3;
UCHAR Type:5;
UCHAR Dpl:2;
UCHAR Present:1;
USHORT OffsetHigh;
}CALL_GATE,*PCALL_GATE;Diagram of the Gate Descriptor Data Structure

BASE23~16 BASE15~0: The segment base address of the segment described by the descriptor.
Limit (segment limit): The offset of the segment’s last byte, which indicates the size of the segment.
A: Whether the segment has been accessed. If the segment has been accessed, A←1; if it has not been accessed, A←0. Combined with the operating system’s clock, this bit can be used in a segment eviction algorithm.
S: Descriptor type. 1 represents a data/code segment descriptor; 0 represents a system descriptor (such as a gate descriptor/task-state-segment descriptor).
DPL: A two-bit field that specifies the lowest privilege level of a task that may access the segment described by this descriptor.
P: 0 indicates that the segment described by this descriptor is not in physical space; 1 indicates that the segment described by this descriptor is in physical space.
TYPE: Consists of three bits: either those for a data segment (E, ED, W) or those for a code segment (E, C, R). This TYPE is extremely important in determining the specific attributes of the memory segment being described.
If the segment is a data segment, E=0, and the ED and W bits of TYPE must also be considered. If ED is 0, the segment grows upward, so the offset must be less than Limit (the segment limit); if ED is 1, the segment grows downward, so the offset must be greater than Limit (the segment limit). If W is 0, the data segment is read-only and cannot be written; if W is 1, the data segment can be read and written.
If the segment is a code segment, E=1, and the C and R bits of TYPE must also be considered (ED becomes C, and W becomes R). If C is 0, the accessing and accessed code segments must have the same privilege level for a nonconforming code-segment access; if C is 1, the accessing and accessed code segments may have different privilege levels for a conforming code-segment access. If R is 0, the code segment can only be executed and cannot be read; if R is 1, the code segment can be both executed and read.

Comments