Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Examples in documentation #60

Closed
AntoineSebert opened this issue Feb 17, 2019 · 14 comments
Closed

Examples in documentation #60

AntoineSebert opened this issue Feb 17, 2019 · 14 comments

Comments

@AntoineSebert
Copy link
Contributor

Hello

I am working with this crate to complete my honours project, but I encountered problems a couple of things.
I looked on the documentation on www.docs.rs, but I could not find any examples, and even worse I am new to Rust, so I'm a bit lost.
How can I add to the IDT an exception handler of the type HandlerFuncWithErrCode ?
Apparently the IDT structure only expects a HandlerFunc handler.
And how would you trigger a custom exception in the code (a kind of throw instruction) ?
Or maybe are you waiting for the stable release 1.0.0 to write the examples ?

@phil-opp
Copy link
Member

Hello, sorry for the missing examples! I try to add some soon.

The CPU only pushes an error code for some exceptions such as the page fault or the general protection fault exception. See https://wiki.osdev.org/Exceptions for an overview over all exception types that shows which exceptions push an error code. This crate tries to enforce this in the IDT type by expecting a HandlerFuncWithErrCode for those exceptions, and a HandlerFunc for the others.

You can trigger a software interrupt through the int instruction. We currently only support int3, which triggers a breakpoint exception, but adding support for other int instructions would be fairly straightforward:

unsafe fn int(number: u8) {
    asm!("int $0" :: "r" (number) :: "volatile");
}

(I didn't test the above code.)

This function should allow you to trigger any interrupt handler (I don't know from the top of my head if it's possible to also trigger exceptions).

I hope this helps!

@AntoineSebert
Copy link
Contributor Author

@phil-opp Thanks for your help !
I read the related article on osdev.org, and it's very complete.
Why does this crate want to enforce HandlerFunc for the custom exceptions ?
I suppose there is a good reason but I cannot figure it out.
I have tried to implement the function as you provided it, but it didn't work, here is what i got :

error: <inline asm>:1:6: error: invalid operand for instruction
        int %al
            ^~~

   --> src\interrupts.rs:132:2
    |
132 |     asm!("int $0" :: "r" (index.as_u8()) :: "volatile");
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I think it is related to the fact that inline asm is still an experimental and buggy feature (issue #29722).
Why do you only support int3 ?
Maybe I could help you to implement a standard interrupt trigger if I manage to create one.

@phil-opp
Copy link
Member

Why does this crate want to enforce HandlerFunc for the custom exceptions ?

HandlerFunc is an alias for a function with the signature:

extern "x86-interrupt" fn(stack_frame: &mut ExceptionStackFrame)

The x86-interrupt calling convention ensures that all registers are restored to their original values before returning and that the error code and the iretq return mechanism are correctly handled. The crate enforces this signature because other calling conventions wouldn't work and lead to undefined behavior.

The only alternative is to use #[naked] functions written in inline assembly, but these are so unsafe that the additionally required (unsafe) cast to a extern "x86-interrupt" fn shouldn't be a problem.

I have tried to implement the function as you provided it, but it didn't work, here is what i got :

Ah, makes sense. The int instruction expects an immediate and not a register operand. So I guess it has to be either a macro or we wait for const generics.

Why do you only support int3 ?

Because nobody implemented other int instructions yet. It seems like const generics are almost there, so it might make sense to wait for it so that we can create a generic int<const N: u8>() function.

@AntoineSebert
Copy link
Contributor Author

@phil-opp I am not sure I understand everything, but I think I got the essential :)
So if I want to pass information with the exception, I can't; if I want to implement the int() function, I wait until the const generics to be here.
Honestly it's a bit of a problem for me, but I can move on to other parts of my project.

I would like to help you with this crate, but I feel when the const generics will be available for use, it will only take you 5mins to implement the int() function. Anyway if there's something simple I can do, tell me.

@phil-opp
Copy link
Member

So if I want to pass information with the exception, I can't

You want to pass arguments to your interrupt handler? There isn't really an easy way to do this because of the fundamental differences between function calls and interrupts: interrupts happen asynchronously, so the caller ofter doesn't even know when an interrupt is invoked, so it can't pass any arguments.

The best way to pass data on synchronous interrupts (software interrupts or system calls), is to put the data into registers before invoking the int instruction and then reading the register value in the interrupt handler. You need to use a naked interrupt handler in this case, because otherwise the function prologue generated by the compiler might already overwrite the register's value. See the source of the Redox kernel for an example how to do it: https://github.com/redox-os/kernel/blob/master/src/arch/x86_64/interrupt/syscall.rs#L89-L143.

if I want to implement the int() function, I wait until the const generics to be here.

Yes, because the int instruction only supports constants as parameter. You can work around this limitation for now by making int a macro instead of a function. The x86 crate implements it that way: https://docs.rs/x86/0.15.1/src/x86/irq.rs.html#275-279. For x86_64, I would rather implement it using const generics, but I'm fine with adding it under a different name if you like.

Let me know if you have further questions!

@AntoineSebert
Copy link
Contributor Author

AntoineSebert commented Feb 26, 2019

There isn't really an easy way to do this because of the fundamental differences between function calls and interrupts: interrupts happen asynchronously

Rust does not support asynchronous tasks ? Or is it part of the standard library so we cannot use it in a no_std context ?
I started to dive in Redox's code a while ago, but I'm struggling to understand what's happeneing in the code ^^

Your proposition of creating an int macro is a bit too ambitious to me, i'll just wait until the const generics to arrive while working on other parts of my OS.

@phil-opp
Copy link
Member

There isn't really an easy way to do this because of the fundamental differences between function calls and interrupts: interrupts happen asynchronously

Rust does not support asynchronous tasks ? Or is it part of the standard library so we cannot use it in a no_std context ?

I'm a bit confused here. Do you want to use interrupts for implementing asynchronous tasks? This won't really work because the int instruction is executed synchronously, i.e. at the moment the int instruction is executed, the CPU directly jumps to the interrupt handler, executes it, and then returns. So it behaves like a normal function call.

Interrupts from the hardware, on the other hand, are asynchronous because they can occur at any time, so that the interrupted code doesn't know when it might be interrupted. Interrupt handlers can't receive any additional arguments because the hardware doesn't set any additional arguments. This isn't a limitation of Rust, but a fundamental property of the x86 architecture. And other architectures do it in a similar way, as far as I know.

If you want to implement an asynchronous task system you have two possibilities:

  • You can use the upcoming async/await feature of Rust. Async/await implements cooperative multitasking, which means that each task runs until its next yield point and can't be preempted before.
  • You can implement your own thread system. This means that you have to create a task_switch function, which switches from one thread to another. This involves saving the current register state, getting the next thread from a scheduler, switching the stack pointer, and restoring the register state of the new thread. Threading is a way to implement preemptive multitasking, i.e. you can switch threads at any time without the need for threads to cooperate.

I hope this helps!

@AntoineSebert
Copy link
Contributor Author

I'm a bit confused here. Do you want to use interrupts for implementing asynchronous tasks?

No no don't worry, it was just a question ^^
The following anwser was helpful tho.

You can use the upcoming async/await feature of Rust. Async/await implements cooperative multitasking, which means that each task runs until its next yield point and can't be preempted before.

I really look forward for this feature to be stable, although it would be more relevant in a computer network context, with a TCP/IP stack, than as a parto of an IPC mechanism.

This involves saving the current register state, getting the next thread from a scheduler, switching the stack pointer, and restoring the register state of the new thread

I think i'll go for this solution, because I need to scheduler to be able to preempt jobs at any time. I'll do more research to understand what I have to implement. Do you think I have to save all the registers ? Does it mean I have to create a struct that is an image of the registers' state ?

@phil-opp
Copy link
Member

phil-opp commented Mar 4, 2019

Yes, you typically want to save all general purpose registers. The reason is that you have to save all registers that the interrupted job uses, so that it can continue later. Note that you need to also save the SSE registers if you enable SSE at some point.

Implementing context switches is a bit tricky because you have to be very careful to not accidentally overwrite registers before saving them. One way to do this is to make the interrupt handler a naked function that does the following:

  • push all registers to the stack
  • call a function that retuns the stack pointer of the next thread from the thread list
  • call a function that puts the current thread (represented by the stack pointer) into the thread list
  • switch the stack by updating the rsp register
  • pop the saved register values of the thread from stack

You probably want to use a different interrupt stack (like we did for the double fault exception) to avoid the race condition between step 3 and 4.

Note that this is only a high-level overview and only one way of doing it. For the blog I plan to use a different approach but I'm not sure if it works out yet.

@AntoineSebert
Copy link
Contributor Author

Oh my gosh I can feel the unsafeness from here.
Also apparently the 0.5.0-alpha.2 of this crate allows to trigger software inteerupts with the macro software_interrupt, which seems to be exactly what I need ! Thanks !

I will read the https://osdev.org articles a bit further and try to create something that works, while keeping your steps as a template. Why do you plan to use a different approach ?

I'll try to figure out how to enable SSE. I don't remember if your OS has it enabled, maybe I could do a pull request once I've figured it out ?

@phil-opp
Copy link
Member

phil-opp commented Mar 7, 2019

Also apparently the 0.5.0-alpha.2 of this crate allows to trigger software inteerupts with the macro software_interrupt, which seems to be exactly what I need ! Thanks !

Yes, I added it a few days ago.

Why do you plan to use a different approach ?

Because of the unsafeness and the need to write assembly code. My idea is to modify the exception stack frame that is passed as an argument to the interrupt handler. The problem is that this doesn't preserve the register values. To solve this, we can add an additional layer by adding an "outer" interrupt handler that does the register saving. In pseudo-code:

fn outer(_) {
   store registers on stack
   invoke `inner` interrupt
   restore registers from stack
}

// does not preserve registers
fn inner(exception_stack_frame) {
    exception_stack_frame = scheduler.next_thread()
}

The disadvantage of this approach is that it is less efficient since some registers are saved in both outer and inner. But I think that it's easier to understand and less error-prone, therefore a good fit for the blog.

@phil-opp
Copy link
Member

phil-opp commented Mar 7, 2019

I'll try to figure out how to enable SSE. I don't remember if your OS has it enabled, maybe I could do a pull request once I've figured it out ?

Enabling SSE is simple: Just set some bit in a configuration register. We deliberately don't enable it because we would then need to save and restore the SSE registers on every interrupt, which is very expensive since the SSE registers are very large. See https://os.phil-opp.com/disable-simd/ for more information.

@AntoineSebert
Copy link
Contributor Author

Alright, thanks for all the info.
Seems pretty complicated, but I'll try to figure it out after I manage to get that silly RTC working (I eventually succeed today but I ain't sure). Do you think I could add it to this crate ?
Also, are the SSE registers that big ?
I'll try to avoid SSE instructions for now, I don't think I really need it anyway, in particular if it brings its own issues.

Concerning the inner/outer interrupts system, why is it not possible to only save the relevant registers ?

@phil-opp
Copy link
Member

t I'll try to figure it out after I manage to get that silly RTC working (I eventually succeed today but I ain't sure). Do you think I could add it to this crate ?

The RTC is a separate controller, right? I think it makes more sense as a separate crate then.

Also, are the SSE registers that big ?

Yes, they are. The 32 zmm registers of the AVX 512 extension are 2KiB together.

Concerning the inner/outer interrupts system, why is it not possible to only save the relevant registers ?

We know nothing about the program that was interrupted, so we don't know which registers hold values that are needed later. For this reason we need to restore all registers to their previous value before continuing it.

The inner function additionally saves and restores some registers because the x86-interrupt calling convention requires that all registers have their original value when returning. This does not suffice for saving the thread registers because a different thread runs when returning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants