2

I'm getting a simple error because I read a document (https://gcc.gnu.org/onlinedocs/gcc/x86-Function-Attributes.html#x86-Function-Attributes) and wanted to do things properly. In this document is stated that:

An interrupt handler must be declared with a mandatory pointer argument:

struct interrupt_frame;

__attribute__ ((interrupt))
void
f (struct interrupt_frame *frame)
{
}

and you must define struct interrupt_frame as described in the processor’s manual.

Exception handlers differ from interrupt handlers because the system pushes an error code on the stack. An exception handler declaration is similar to that for an interrupt handler, but with a different mandatory function signature. The compiler arranges to pop the error code off the stack before the IRET instruction.

#ifdef __x86_64__
typedef unsigned long long int uword_t;
#else
typedef unsigned int uword_t;
#endif

struct interrupt_frame;

__attribute__ ((interrupt))
void
f (struct interrupt_frame *frame, uword_t error_code)
{
 ...
}

Exception handlers should only be used for exceptions that push an error code; you should use an interrupt handler in other cases. The system will crash if the wrong kind of handler is used.

Exceptions that push an error code should thus have a 64 bits long as the second argument. Others should have only the user defined interrupt_frame struct. I thus attempted to do that with the following code:

struct InterruptFrame{
    UINT64 rsp;
};

//Divide by zero error
__attribute__((interrupt)) void IDT::isr0(InterruptFrame* frame) {
    asm volatile("hlt");
}

//Page fault
__attribute__((interrupt)) void IDT::isr14(InterruptFrame* frame, unsigned long long int errorCode) {
    asm volatile("hlt");
}

Those are just examples because I have more separate ISRs. When I compile with

g++ -static -ffreestanding -nostdlib -mgeneral-regs-only -mno-red-zone -c -m64 Kernel/Source/IDT.cpp -oKernel/Object/IDT.o

I get an error stating

error: interrupt service routine should have ‘unsigned long int’ as the second argument

even though I did exactly as mentioned in documentation. I get the error for each ISR even for the page fault handler which currently has an unsigned long long int as a second argument. I didn't find much searching on the web for the error so I thought of asking here.

What is wrong? Also, what members should the InterruptFrame struct contain for x86-64?

user123
  • 2,510
  • 2
  • 6
  • 20

2 Answers2

1

What is wrong?

I think the error message seems fairly clear:

error: interrupt service routine should have ‘unsigned long int’ as the second argument

Based on that error message, I recommend solving the problem by having ‘unsigned long int’ as the second argument instead of unsigned long long int.

The source code of the error message reveals the conditions for which type to use:

gcc/config/i386/i386-options.c

  error ("interrupt service routine should have %qs "
     "as the second argument",
     TARGET_64BIT
     ? (TARGET_X32 ? "unsigned long long int"
           : "unsigned long int")
     : "unsigned int");

So, it seems that unsigned int should be used when targeting 32 bit x86, unsigned long long should be used when targeting the 32 bit ABI of 64 x86_64 and unsigned long when targeting 64 bit ABI of x86_64.


Also, what members should the InterruptFrame struct contain for x86-64?

I don't think you necessarily need to define the struct until you intend to access it. But in case you do, the AMD64 Architecture Programmer’s Manual Volume 2: System Programming says:

8.9 Long-Mode Interrupt Control Transfers

8.9.3 Interrupt Stack Frame

Interrupt-Handler Stack

Above that, there is detailed description about the contents.

Based on the diagram, following seems to match (Untested. I recommend reader to verify whether the order is correct or reversed):

struct interrupt_frame
{
    std::uint64_t instruction_pointer;
    std::uint64_t code_segment;
    std::uint64_t rflags;
    std::uint64_t register_stack_pointer;
    std::uint64_t stack_segment;
}
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • unsigned long long int and unsigned long int are the same and GCC actually uses unsigned long long int in the documentation for `interrupt` attribute – Michael Petch Aug 19 '21 at 00:12
  • 1
    @MichaelPetch No, they aren't "the same". They are different types even when they have the same size. Regardless of the documentation, GCC reportedly says: `error: interrupt service routine should have ‘unsigned long int’ as the second argument` – eerorika Aug 19 '21 at 00:14
  • What I didn't notice until just now that this was C++(G++) and not C – Michael Petch Aug 19 '21 at 00:19
  • I tried most of that. But supposedly the error still happens and, according to documentation, gcc/g++ should recognize the signatures. If they have the first signature (without error code) it should not pop an error code off the stack. Meanwhile, if they have the second they should. I'll try C style structs see if it works (and call it the same). Maybe thats the culprit. – user123 Aug 19 '21 at 00:31
  • @user123 I doubt the name of the struct matters, but worth a try. Are you saying that if you use `void(interrupt_frame*, unsigned long int)`, that you get the error message: `error: interrupt service routine should have ‘unsigned long int’ as the second argument`? That would be quite strange. – eerorika Aug 19 '21 at 00:38
  • I'll try that again in a separate file when I'm home. I think I do because I'm quite sure I actually tested with unsigned long int also. The problem is that I get the error for all ISRs including the ones which shouldn't have an error code pushed on the stack. This is a problem because, if the processor doesn't push an error code, I can't modify that behavior. Will the code still try to pop an error code and mess up my stack? In the end, I may just do like Linux and write an assembly stub that's called from my code and declared as extern. I'd still like it to work though, so I'll try. – user123 Aug 19 '21 at 00:45
  • @user123 Ah, getting the error for the handler that accepts only one parameter would certainly be a problem. I don't know what could cause that. – eerorika Aug 19 '21 at 00:49
  • I think the problem is that the ISRs are static members of the IDT class. Could that be an issue in any way? – user123 Aug 19 '21 at 01:20
  • If I compile the code without embedding the ISRs in an object, everything works fine. – user123 Aug 19 '21 at 01:21
  • Actually, if I compile the code without embedding the ISRs in an object, I can have any name for the struct and 'unsigned long long int' as well as 'unsigned long int' as well as 'unsigned long' all work. – user123 Aug 19 '21 at 01:23
  • @user123 Oh, you were using non-static member functions? That would explain the problem. – eerorika Aug 19 '21 at 09:07
1

In the end, the problem was a minor mistake of mine. I declared the __attribute__((interrupt)) on the declaration of the C++ IDT member functions instead of on the prototypes. You can reproduce the issue with the following:

struct InterruptFrame{
    unsigned long rsp;
};

class IDT{
public:
    static void isr0(InterruptFrame* frame);
    static void isr14(InterruptFrame* frame, unsigned long errorCode);
};

__attribute__((interrupt)) void IDT::isr0(InterruptFrame* frame) {
    asm volatile("hlt");
}

__attribute__((interrupt)) void IDT::isr14(InterruptFrame* frame, unsigned long errorCode) {
    asm volatile("hlt");
}

This file produces an error. Whereas the following file doesn't produce any error.

struct InterruptFrame{
    unsigned long rsp;
};

class IDT{
public:
    __attribute__((interrupt)) static void isr0(InterruptFrame* frame);
    __attribute__((interrupt)) static void isr14(InterruptFrame* frame, unsigned long errorCode);
};

void IDT::isr0(InterruptFrame* frame) {
    asm volatile("hlt");
}

void IDT::isr14(InterruptFrame* frame, unsigned long errorCode) {
    asm volatile("hlt");
}

I can disassemble the code and see that the interrupt code seems correct:

user@user-System-Product-Name:~$ objdump -d test.o

test.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_ZN3IDT4isr0EP14InterruptFrame>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   50                      push   %rax
   9:   48 83 ec 08             sub    $0x8,%rsp
   d:   48 8d 45 08             lea    0x8(%rbp),%rax
  11:   48 89 45 f0             mov    %rax,-0x10(%rbp)
  15:   f4                      hlt    
  16:   90                      nop
  17:   48 83 c4 08             add    $0x8,%rsp
  1b:   58                      pop    %rax
  1c:   5d                      pop    %rbp
  1d:   48 cf                   iretq  
  1f:   90                      nop

0000000000000020 <_ZN3IDT5isr14EP14InterruptFramem>:
  20:   f3 0f 1e fa             endbr64 
  24:   55                      push   %rbp
  25:   48 89 e5                mov    %rsp,%rbp
  28:   50                      push   %rax
  29:   48 83 ec 10             sub    $0x10,%rsp
  2d:   48 8d 45 10             lea    0x10(%rbp),%rax
  31:   48 89 45 f0             mov    %rax,-0x10(%rbp)
  35:   48 8b 45 08             mov    0x8(%rbp),%rax
  39:   48 89 45 e8             mov    %rax,-0x18(%rbp)
  3d:   f4                      hlt    
  3e:   90                      nop
  3f:   48 83 c4 10             add    $0x10,%rsp
  43:   58                      pop    %rax
  44:   5d                      pop    %rbp
  45:   48 83 c4 08             add    $0x8,%rsp
  49:   48 cf                   iretq

I still wonder why doesn't the interrupt code push all registers?

user123
  • 2,510
  • 2
  • 6
  • 20
  • 1
    It will push the registers that are used. In this case RAX is used so it is saved and restored. Unfortunately this may not be the desired behaviour for interrupt handlers that may wish to know what the state of all the registers were when the interrupt occurred. This is actually a downside for some and often why I don't use it. I do provide one GCC work around here: https://stackoverflow.com/a/43311525/3857942 . Some prefer a more straight forward method by doing their interrupt stubs in assembly and have them call into _C_ functions. – Michael Petch Aug 19 '21 at 22:20
  • The major mistake was using this ridiculous attribute extension thinking it will help you. Did you not notice you were using this inanity to make a function with an assembly statement in it? Why wouldn't your write it in assembler? At this point, assembly is actually more portable, as it is merely machine specific, rather than toolchain + version + flags + unspecified-behaviour specific? – mevets Aug 20 '21 at 03:17
  • The assembly statement is for halting the processor before I make the IDT do something useful. I'm probably going to use an assembly stub in the end. – user123 Aug 20 '21 at 04:30
  • 1
    The assembly stub actually makes it more complex than writing it in C++ all the way. This is why I tried to make it work. It seems like the attribute doesn't do what it should or at least not what I would lilke. In the end, I'm going to use an assembly stub. – user123 Aug 20 '21 at 04:38
  • By the way, assembly is quite toolchain + version etc specific. If you use the GNU toolchain to write assembly then you need to write it in GNU syntax meanwhile if you use nasm and company you need to write it in Intel syntax. The Linux kernel actually uses GNU syntax you thus can't compile the Linux's assembly using a lot of programs. – user123 Aug 20 '21 at 04:41
  • Oh I agree it is easier to do it in assembly. It is a giant hack to shoehorn it in from C++. It is also possible to call the static members of a class for the interrupt callbacks but that also involves generating all the mangled C++ names and calling them from the stubs. – Michael Petch Aug 20 '21 at 07:50
  • In the end I just declared a function ```extern "C"``` to avoid name mangling (outside of a class) and called the function from assembly. I'm still working on the details but once it is done I may edit the answer to provide the code. – user123 Aug 20 '21 at 08:43
  • extern "C" will work, although the callback handlers will not be coupled with the classes. I have used the name mangling method and generating the objects containing the classes and then using `objdump` to dump the mangled names and then I use those in the assembly code. (you could compile with `-S` to get the assembly as well with mangled names). Not if you are using C++ and have global objects with global constructors you need to make sure the constructor table is built (usually done with a linker script) and then calling all the functions before entering C++ kernel code. – Michael Petch Aug 20 '21 at 18:18
  • The interrupt code will be outside of a class. I mentionned it between parenthesis in my previous comment. Otherwise it will get too complex. – user123 Aug 20 '21 at 20:00
  • I know the global constructors need to be called at some point but I avoid it by setting my entry point as main and by setting the static objects in main (or the functions it calls). – user123 Aug 20 '21 at 20:15