[x64] can not switch from ring3 to ring0 by call gate

Programming, for all ages and all languages.
Post Reply
CandyMan
Posts: 6
Joined: Sat Aug 27, 2011 9:25 am

[x64] can not switch from ring3 to ring0 by call gate

Post by CandyMan »

can not switch from ring3 to ring0 by call gate, code below generating invlid TSS exception, but via syscall switch works fine. where is bug?

Code: Select all

use64
        call    [PL3toPL0]
NextPL3PL0:
        ...

PL3toPL0        dp Gate386-GDT+3:0

GDT:    dq 0

        ; 32-bit code descriptor 0x08
Code32  dw 0xFFFF                       ; limit low
        dw 0                            ; base low
        db 0                            ; base middle
        db 10011010b                    ; access
        db 11001111b                    ; flags and limit high
        db 0                            ; base high

        ; 32-bit data descriptor 0x10
Data32: dw 0xFFFF
        dw 0
        db 0
        db 10010010b
        db 11001111b
        db 0

        ; 16-bit code descriptor 0x18
Code16  dw 0xFFFF
        dw 0
        db 0
        db 10011010b
        db 10001111b
        db 0

        ; 16-bit data descriptor 0x20
Data16  dw 0xFFFF
        dw 0
        db 0
        db 10010010b
        db 10001111b
        db 0

        ; 64-bit kernel code descriptor 0x28
Code64  dw 0xFFFF
        dw 0
        db 0
        db 10011010b
        db 10101111b
        db 0

        ; 64-bit kernel data descriptor 0x30
Data64  dw 0xFFFF
        dw 0
        db 0
        db 10010010b
        db 11001111b
        db 0

        ; Usermode code descriptor 0x38
Code643 dw 0xFFFF
        dw 0
        db 0
        db 11111010b
        db 10101111b
        db 0

        ; Usermode data descriptor 0x40
Data643 dw $FFFF
        dw 0
        db 0
        db 11110010b
        db 11001111b
        db 0

        ; Usermode code descriptor 0x48
Code64X dw 0xFFFF
        dw 0
        db 0
        db 11111010b
        db 10101111b
        db 0

        ; TSS descriptor 0x50-0x58
Tss64   dw 68h+IOLIMIT
        dw (TSS64 and 0xFFFF)
        db (TSS64 shr 16)and 0xFF
        db 11101001b
        db 00100000b
        db (TSS64 shr 24)and 0xFF
        dd (TSS64 shr 32)
        dd 0

        ; 32-bit user descriptor 0x60
Code323 dw 0xFFFF
        dw 0
        db 0
        db 11111010b
        db 11001111b
        db 0

Gate386 dw (NextPL3PL0 and 0xFFFF)
        dw Code64-GDT
        db 0
        db 11101100b
        dw (NextPL3PL0 shr 16)and 0xFFFF
        dd (NextPL3PL0 shr 32)
        dd 0
simeonz
Member
Member
Posts: 360
Joined: Fri Aug 19, 2016 10:28 pm

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by simeonz »

db 00100000b, should be db 00010000b or db 00000000b. You are setting the long mode flag for the TSS, but it needs to be always cleared (even for 64-bit TSS)
User avatar
Schol-R-LEA
Member
Member
Posts: 1925
Joined: Fri Oct 27, 2006 9:42 am
Location: Athens, GA, USA

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by Schol-R-LEA »

TBH, I am not quite certain why you are having this specific issue, but I'll dig deeper shortly.

Having said that: I know that this has been said to you before, but I think it needs to be reiterated that the Duct von Tape approach to writing programs is not a viable strategy in OS dev. This problem, or rather the several problems I see in your GDT, is a good example of why this is the case.

I am not sure how much of this code you wrote yourself, if any, but I can say for certain that you haven't understood all of it. The reason I say this is because
  • You don't appear to have a null descriptor, which is the one things that is unequivocally necessary for a valid GDT; or rather, you don't explicitly refer to it as such. While you do allocate the space for it, here,

    Code: Select all

    GDT:    dq 0
    the fact that it isn't labelled as such makes me think you don't understand why you are allocating that space. I am hoping I am wrong on this, but even if I am, not having an explicit Null entry is at the very least misleading, and poor coding practice (your coding standards are your choice, of course, but I would advise being explicit here).
  • Every single one of the GDT descriptors has the same set of access settings, 10011010b. Let's break this down, for each of the values you have set, following the wiki page on the Global Descriptor Table and the Intel manuals (also, note that some of the names in the wiki differ from those in the manuals), with the bits numbered for the Access Byte (rather than for the descriptor as a whole):
    • Bit 7, P: 1 - Present flag. When set, as you have it in your descriptors, it means the segment is present in memory (both in the sense of there being physical memory for it, and that it is paged in).
    • Bits 6-5, DPL: 00 - Descriptor Privilege Level field, a pair of bits indicating the privilege level needed to access or execute the segment. Both bits cleared means that the segment has CR0 (kernel-only) access.
    • Bit 4, S: 1 - Segment type (system vs code/data). Set (as it is in this case), this indicates that the segment is either user code or user data. If it is cleared, then it is a system segment, and the Type field is ignored.
    • Bits 3-0. T: 1010 - the Type field. In the manuals, the next four flags because they are only used if the Segment Type flag is set. The wiki treats them as independent flags, which for most purposes is going to be more meaningful, IMAO.
      • Bit 3, X: 1 - Executable flag. 1 means it is a Code segment - values in the segment will be treated as executable instructions.
      • Bit 2, E/C: 0 - Expand/Conform (given as 'Direction/Conformance' in the wiki) flag union. This one is a union field, meaning it means different things depending on whether the segment is code or data. For a code segment, 1 means that the segment can be executed by processes with the same Privilege or lower level as the segment. Since that is Kernel in this case, it means that any process can execute it. For data, setting it to 1 means that the segment 'grows' down in memory; as a rule, only stack segments should be set to grow down.
      • Bit 1, R/W: 1 - Read/Write flag. As with DC, this is a union flag, with different interpretations for code and data segments. A 1 will mean read access for a code segment (but never write access; code segments are always read-only). For data segments (ones with the E bit cleared), it indicates both Read and Write access.
      • Bit 0, A: 0 - Accessed flag. This indicates that nothing has actually used the segment yet. As soon as that happened (either by executing code, reading data, or writing data), the flag gets set. This is mostly used when implementing virtual memory systems, IIUC.
    (IAN an expert on this, so C&CW.)
    So, what does all of this mean? It means all of the segments - including those you mean to be system or data - are listed as being "User Code, Present in memory, Readable but not writable, accessible only to kernel code, and not yet accessed." I am pretty sure that the at least some of the segments should be set differently, even though all of these overlap exactly.
  • Similarly, let's take a look at the Segment Scaling Definition flags (called 'Flags' in the wiki), which are also all the set to the same values in your code (110xxxxx):
    • Bit 7, G: 1 - Granularity, which decides how the segment limits are scaled. Setting this to 1 indicates that you are using segment limits with a stride of 4 KiB (that is to say, a limit of 0x00001 would be a 4 KiB segment; a limit if 0x00004 would be a 16 KiB segment, etc.).
    • Bits 6, D/B: 1 - Default Argument Size/Big Segment flag. For code, when set (as it is here), this indicates that the default sizes for instruction operands and addresses are either 32-bit or 8-bit, while when cleared it means they are 16-bit or 8-bit. For data segments, it indicates whether it is a 64KiB maximum size segment, or a 4 GiB maximum size segment.
    • Bit 5, L: 0 - Long mode flag. Since it is cleared, it means you are in 32-bit protected mode, instead of 64-bit protected mode. When set, this further modifies the meanings of the G and D/B flags to indicate 64-bit values where it would have been 32-bit ones. The Intel manuals seem to indicate that this can in fact still be set even if the system as a whole is in 32-bit mode, but I am pretty sure things get weird if you do that... If anyone can add more on this, feel free.
    Since you are also setting all the segments to long strides, and all of them have a base of 0x00000000 and a limit of 0xFFFFF, this would mean that all of the segments are 4 GiB in total size, and all overlap each other exactly.
The implications of these facts are left as an exercise for the reader.
Last edited by Schol-R-LEA on Mon Jun 18, 2018 8:00 am, edited 4 times in total.
Rev. First Speaker Schol-R-LEA;2 LCF ELF JAM POEE KoR KCO PPWMTF
Ordo OS Project
Lisp programmers tend to seem very odd to outsiders, just like anyone else who has had a religious experience they can't quite explain to others.
User avatar
Schol-R-LEA
Member
Member
Posts: 1925
Joined: Fri Oct 27, 2006 9:42 am
Location: Athens, GA, USA

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by Schol-R-LEA »

simeonz wrote:db 00100000b, should be db 00010000b or db 00000000b. You are setting the long mode flag for the TSS, but it needs to be always cleared (even for 64-bit TSS)
OK, that would probably do it. There may be other issues as well, but that alone would be a showstopper.

I will add something that occurred to me earlier: the OP would do well to look at the macro and structure-definition features of their assembler (I am guessing it is NASM, but I am not sure) as a way of simplifying these declarations, both for avoiding repetition (by using the default initializations, if it allows them) and for making the setting process more meaningful by giving names to the fields and so forth.

You could, for example, have a struct definition like this NASM version:

Code: Select all

struc GDT_Entry
    .limit_0_15        resw 1
    .base_0_15         resw 1
    .base_16_23        resb 1
    .access            resb 1
    .flags_limit_16_19 resb 1
    .base_24_31        resb 1
endstruc
Which would at least let you then do this:

Code: Select all

Pr_Shift equ 7
DPL_Shift equ 5
ST_Shift equ 4
Ex_Shift equ 3
DC_Shift equ 2
RW_Shift equ 1
Ac_Shift equ 0

G_Shift equ 7
DB_Shift equ 6
L_Shift equ 5

istruc GDT_Entry
    at .limit_0_15,        dw 0xFFFF
    at .base_0_15,         dw 0x0000
    at .base_16_23,        db 0x00
    at .access,            db (1 << Pr_Shift) & (0 << DPL_Shift) &  (1 << ST_Shift) & (1 << Ex-Shift) & (0 << DC_Shift) & (1 << RW_Shift) & (0 << Ac_Shift)
    at .flags_limit_16_19, db  0x0F & (1 << G_Shift) & (1 << DB_Shift) & (0 << L_Shift)
    at .base_24_31,        db 0x00
iend
And then have a macro that takes the actual values you want by name, rather than the byte and bit values, and automagically combine them into the fields for you:

Code: Select all

%macro Make_GDT_Entry 0-10 0, 1, 1, 0, 1, 0, 1, 1, 0, 0x00000000, 0x00FFFFFF 
istruc GDT_Entry
    at .limit_0_15,        dw (%9 & 0x000FFFF)
    at .base_0_15,         dw (%10 & 0x0000FFFF)
    at .base_16_23,        db ((%9 & 0x00FF0000) >> 16)
    at .access,            db 0x80 & (%0 << DPL_Shift) &  (%1 << ST_Shift) & ((%2  & %0) << Ex-Shift) & ((%3 & %0)  << DC_Shift) & ((%4 & %0) << RW_Shift)  & ((%5  & %0) << Ac_Shift)
    at .flags_limit_16_19, db  ((%10 & 0x00FF0000) >> 16) & (%6 << G_Shift) & (%7 << DB_Shift) & (%8 << L_Shift)
    at .base_24_31,        db ((%9 & 0xFF000000) >> 24)
iend
%endmacro
Which, if used with just the defaults,

Code: Select all

GDT: 
NULL_Descriptor:   dq 0

        ; 32-bit code segment descriptor
Code32:
       Make_GDT_Entry
would give you the descriptor you were using. For a System segment, you could use,

Code: Select all

        ; 32-bit kernel segment descriptor
Kernel32:
    Make_GEDT_Entry 0
which would be the same, except it would leave S, X, DC, RW, and Ac cleared.

(Note that this isn't tested code, so I may have some mistakes in it.)

You could, of course, make more specialized macros at your discretion. You would naturally need to adjust these to fit you assembler.
Last edited by Schol-R-LEA on Fri Jun 15, 2018 10:09 am, edited 1 time in total.
Rev. First Speaker Schol-R-LEA;2 LCF ELF JAM POEE KoR KCO PPWMTF
Ordo OS Project
Lisp programmers tend to seem very odd to outsiders, just like anyone else who has had a religious experience they can't quite explain to others.
simeonz
Member
Member
Posts: 360
Joined: Fri Aug 19, 2016 10:28 pm

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by simeonz »

Schol-R-LEA wrote:I will add something that occurred to me earlier: the OP would do well to look at the macro and structure-definition features of their assembler (I am guessing it is NASM, but I am not sure) as a way of simplifying these declarations, both for avoiding repetition (by using the default initializations, if it allows them) and for making the setting process more meaningful by giving names to the fields and so forth.
True. I was thinking the same thing while counting the bit positions. :) The error may not have occurred, assuming this is the error, if the data was more "literate". Since I am not myself an avid assembler enthusiast, I am curious. Macros are somewhat less expressive then structures/records, so may we assume that the advice is to use NASM (for portability sake)? Or is using gas with a stronger preprocessor (like m4) a better option? Or NASM with m4? Or C/C++ with mostly inline assembler?
User avatar
Schol-R-LEA
Member
Member
Posts: 1925
Joined: Fri Oct 27, 2006 9:42 am
Location: Athens, GA, USA

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by Schol-R-LEA »

BTW, I noticed a mistake in the earlier post, which I've now corrected; I had the last part of base field declared as word, when it should be byte.
simeonz wrote:
Schol-R-LEA wrote:I will add something that occurred to me earlier: the OP would do well to look at the macro and structure-definition features of their assembler (I am guessing it is NASM, but I am not sure) as a way of simplifying these declarations, both for avoiding repetition (by using the default initializations, if it allows them) and for making the setting process more meaningful by giving names to the fields and so forth.
True. I was thinking the same thing while counting the bit positions. :) The error may not have occurred, assuming this is the error, if the data was more "literate". Since I am not myself an avid assembler enthusiast, I am curious. Macros are somewhat less expressive then structures/records, so may we assume that the advice is to use NASM (for portability sake)? Or is using gas with a stronger preprocessor (like m4) a better option? Or NASM with m4? Or C/C++ with mostly inline assembler?
I wasn't really trying to make a recommendation, and honestly, I agree that NASM macros are less expressive than NASM structs, or for that matter, macros for other assemblers. I was using NASM because it seems to be the one most often used for x86 OS dev, and I am more familiar with it than, say, FASM.

A stronger macro preprocessor would definitely be a good idea; I've used m4 to good effect in the past myself (though it was an odd situation¹ and I misused it somewhat), and have even tried using the stand-alone version of cpp (it works, but it isn't terribly well suited for assembly work).

I would be hesitant to suggest using C, simply because I don't know what CandyMan's goals and intentions are - if they are planning to use assembly across the board, the recommendation probably wouldn't go over terribly well.

In any case, I wouldn't recommend using inline assembly extensively or exclusively for large amounts of assembly work. However, for the GDT and related system data structures, there's no reason C can't be used directly for a large part of it so long as you remember to set the alignments and packing using the GCC attribute pragma or the equivalent thereof for the given compiler. For GCC, this would be

Code: Select all

typedef struct
{
    uint16_t limit_0_15, base_0_15;
    uint8_t base_16_23;
    uint8_t access;
    uint8_t flags_limit_16_19;
    uint8_t base_24_31;
endstruc
}  __attribute__ ((aligned(8), packed )) GDT_Entry;
I am pretty sure that the alignment should be 8 for both 32-bit and 64-bit modes, but if anyone can confirm or refute this I would appreciate it. Also, I separated the declarations of the access and flags_limit_16_19 bytes, rather than lumping all the uint8_t declarations together, in case someone wants to try messing about with bit fields (which can be dicey in general, WRT differences in compilers, but if you are sticking to a specific compiler with a known bit ordering and implementation of the bit accesses, it would make working with the individual fields much easier).

While the wiki's GDT Tutorial doesn't have an example of this (even though it uses C for most of the example code), there are several threads in the forum where that approach is discussed.

Footnote
1. It was for an assembly language course that used MIPS as the target, and SPIM as the emulator. SPIM's built in assembler is severely limited, lacking even the most rudimentary extra features such as structures, macros, or even support for multiple source files, so when I had a large-ish final project - a Sudoku solver - I convinced my lab partner to use m4 to make the work easier. It was still a hassle - we had to manually preprocess the macro-fied source file separately into a final source file, which made it a bit annoying to test the code - but it worked well, or would have if I hadn't started overdoing things with the macrology to the point where it started getting more confusing rather than less.
Rev. First Speaker Schol-R-LEA;2 LCF ELF JAM POEE KoR KCO PPWMTF
Ordo OS Project
Lisp programmers tend to seem very odd to outsiders, just like anyone else who has had a religious experience they can't quite explain to others.
User avatar
Schol-R-LEA
Member
Member
Posts: 1925
Joined: Fri Oct 27, 2006 9:42 am
Location: Athens, GA, USA

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by Schol-R-LEA »

Sorry for the sequential posts, but I just want to clarify a point: if my reading of the segment descriptors earlier was correct, and the code isn't changing them or a adding any new ones, then it shouldn't be possible for you to be running anything in ring 3 to begin with - I am pretty sure that a DPL of 00 across the board means those segments are all set to ring 0 only access.

Can anyone confirm or refute this? Have I missed any details on that? As I've mentioned before, this is an area I have not really delved into.

@CandyMan: Could you provide a link to your offsite repo so we can see the whole code if we need to? If you don't have any offsite repo... well, in that case, you don't want do anything else on your OS until you've set one up. Trust me on this; they are too darn useful not to have for even a small project.
Rev. First Speaker Schol-R-LEA;2 LCF ELF JAM POEE KoR KCO PPWMTF
Ordo OS Project
Lisp programmers tend to seem very odd to outsiders, just like anyone else who has had a religious experience they can't quite explain to others.
simeonz
Member
Member
Posts: 360
Joined: Fri Aug 19, 2016 10:28 pm

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by simeonz »

Schol-R-LEA wrote:I am pretty sure that a DPL of 00 across the board means those segments are all set to ring 0 only access.
I actually see a lot of DPL 3 segment descriptors. Also, your breakdown of the bits was correct, but if you go to Code643, the flags are 11111010b, which means present, DPL = 11b, etc. I am not sure what the 64-bit data segment descriptors are for, but may be unused.
User avatar
Schol-R-LEA
Member
Member
Posts: 1925
Joined: Fri Oct 27, 2006 9:42 am
Location: Athens, GA, USA

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by Schol-R-LEA »

simeonz wrote:
Schol-R-LEA wrote:I am pretty sure that a DPL of 00 across the board means those segments are all set to ring 0 only access.
I actually see a lot of DPL 3 segment descriptors. Also, your breakdown of the bits was correct, but if you go to Code643, the flags are 11111010b, which means present, DPL = 11b, etc. I am not sure what the 64-bit data segment descriptors are for, but may be unused.
facepalm I could have sworn I had checked all of them and that they were all identical. .. that was careless of me.
Rev. First Speaker Schol-R-LEA;2 LCF ELF JAM POEE KoR KCO PPWMTF
Ordo OS Project
Lisp programmers tend to seem very odd to outsiders, just like anyone else who has had a religious experience they can't quite explain to others.
simeonz
Member
Member
Posts: 360
Joined: Fri Aug 19, 2016 10:28 pm

Re: [x64] can not switch from ring3 to ring0 by call gate

Post by simeonz »

Schol-R-LEA wrote:facepalm I could have sworn I had checked all of them and that they were all identical. .. that was careless of me.
There are a lot of numbers, and little text. I am not attacking the OP or anything, but indeed, it is hard not to miss a thing or two, as they themselves have discovered, I think.
Post Reply