Naked functions

I'd love to be able to write a _start function in Swift and I believe this would be possible with naked functions. Unsafe, but possible.

I have managed to write a Swift _start that compiles equivalent to the hand-written version:

import KernelKit
public import EmbeddedArch

@c(_start) @section(".text.boot")
func _start() {
	let reg = get_mpidr_el1()
	// read cpu id, stop slave cores
	if reg.Aff0 & 0x3 == 0 {
		primary()
	}
	hang()
}

@_transparent
private func primary() {
	let elr = get_CurrentEL()
	switch elr.el {
	case 2:
		var hcr = HCR()
		hcr.rw = true
		set_hcr_el2(hcr)

		zero_cntvoff_el2()

		var spsr = SPSR()
		// EL1h
		spsr.m_4_0_ = 5
		// mask DAIF
		spsr.e = true
		spsr.a = true
		spsr.i = true
		spsr.f = true
		// set return level to EL1
		set_spsr_el2(spsr)

		set_elr_el2(jump_to_main)
		isb_eret()
	case 3:
		var hcr = HCR()
		hcr.rw = true
		set_hcr_el2(hcr)

		var scr = SCR_EL3()
		// disable trapping of timer control registers
		scr.rw = true
		// next lower EL in aarch64
		scr.st = true
		set_scr_el3(scr)

		var spsr = SPSR()
		// EL1h
		spsr.m_4_0_ = 5
		// mask DAIF
		spsr.e = true
		spsr.a = true
		spsr.i = true
		spsr.f = true
		// set return level to EL1
		set_spsr_el3(spsr)

		set_elr_el3(jump_to_main)
		isb_eret()
	default:
		break
	}
}

@c
private func jump_to_main() {
	//FIXME: uses stp        fp, lr, [sp, #-0x10]!
	set_sp(ImageLayout.topOfStack)
	RaspberryPi.main()
	hang()
}

But sometimes there are instructions which unfortunately make this completely unviable.

stp        fp, lr, [sp, #-0x10]!
mov        fp, sp
// Set control registers, stack, exceptions, interrupts, etc.
ldp        fp, lr, [sp], #0x10
// Jump to Kernel's main

Sometimes I can get rid of them via refactoring to ensure everything stays in registers.
Sometimes I can't and the very first instruction causes the processor to fall into a trap loop.

I'd like to suggest adding the equivalent of [[gnu::naked]] (also known as __attribute__((naked)) the legacy syntax) in C.
I'm imagining something like @naked in the same vein as @c and @objc.

It could also require the Embedded feature if people think it shouldn't be too easy to reach for in normal Swift code.

Thoughts?

Assembly for the curious
                     EntryPoint:
0000000000080000         mrs        x8, mpidr_el1                               ; DATA XREF=sub_8141c+96
0000000000080004         tst        x8, #0x3
0000000000080008         b.ne       loc_80024

000000000008000c         mrs        x8, currentel
0000000000080010         ubfx       x8, x8, #0x2, #0x2
0000000000080014         cmp        x8, #0x3
0000000000080018         b.eq       loc_8002c

000000000008001c         cmp        x8, #0x2
0000000000080020         b.eq       loc_80058

                     loc_80024:
0000000000080024         wfi                                                    ; CODE XREF=EntryPoint+8, EntryPoint+40
0000000000080028         b          loc_80024

                     loc_8002c:
000000000008002c         mov        w8, #0x80000000                             ; CODE XREF=EntryPoint+24
0000000000080030         mov        w9, #0xc00
0000000000080034         mov        w10, #0x3c5
0000000000080038         msr        hcr_el2, x8
000000000008003c         msr        scr_el3, x9
0000000000080040         nop
0000000000080044         adr        x8, #0x81e38
0000000000080048         msr        spsr_el3, x10
000000000008004c         msr        elr_el3, x8
0000000000080050         isb
0000000000080054         eret
                     loc_80058:
0000000000080058         mov        w8, #0x80000000                             ; CODE XREF=EntryPoint+32
000000000008005c         nop
0000000000080060         adr        x9, #0x81e38
0000000000080064         mov        w10, #0x3c5
0000000000080068         msr        hcr_el2, x8
000000000008006c         msr        cntvoff_el2, xzr
0000000000080070         msr        spsr_el2, x10
0000000000080074         msr        elr_el2, x9
0000000000080078         isb
000000000008007c         eret


                     sub_81e38:
0000000000081e38         stp        fp, lr, [sp, #-0x10]!                       ; DATA XREF=EntryPoint+68, EntryPoint+96
0000000000081e3c         mov        fp, sp
0000000000081e40         nop
0000000000081e44         adr        x8, #0xa33d0
0000000000081e48         mov        sp, x8
0000000000081e4c         bl         sub_81784                                   ; sub_81784
                     loc_81e50:
0000000000081e50         wfi                                                    ; CODE XREF=sub_81e38+28
0000000000081e54         b          loc_81e50

Precise control over the calling convention and register usage is useful for writing efficient interpreters as well. It’s completely unsafe, but of course the idea is you isolate it a tiny part of the main loop.

When I was playing around with this kind of thing in gcc years ago, I remember they had various extensions such as taking the address of labels and global register variables. They were all exceptionally flaky. Perhaps there are better ways of expressing these features that while still unsafe, are somewhat easier to use.

To make this concrete: I don’t know how this plays with modern CPUs and security mitigations, but what I’m imagining is that instead of looping over your bytecode and performing a subroutine call for each primitive, you store the program as a list of function pointers. Each “instruction” function expects to find the instruction pointer in a fixed register. At the end of each such function, instead of returning, you increment the instruction pointer, read from it, and do an indirect tail call to the next address.

3 Likes

This does require guaranteed tail calls though, but I suppose if you're already willing to drop down to assembly you can just write the JMP yourself. Rust, on Nightly, use the become keyword for this, but I'm not sure if Swift has the same problems with deinit ordering that Rust has.

Rust also only supports tail-calls to functions with the same argument and return type as the calling function, but if we're using custom calling conventions that can be fixed by just having an extra stack for parameters. The return type would still have to be the same, but arguments wouldn't matter at all.

Yeah, you would presumably need a guaranteed tail call annotated as such. Perhaps each such function's return type is Never in this setup, and it either has no arguments if your VM instruction/stack pointers go in global register variables; or alternatively each function takes those pointers as arguments, in which case they would logically be forwarded to each call.

1 Like

Never returning functions have been how I've gotten tail calls so far, at worse I've had trailing ret instructions after a call but if it becomes a problem I can always drop down to assembly just for the call or pre-cleanup of the stack pointer for missing epilogues.

1 Like

EDIT: nevermind I got it, it had to be __asm__ inline volatile ("bl %0" :: "i" (r));!

I've tried with the following for AArch64

// C code to jump without Swift knowing
inline void bl(addr_t r) {
	__asm__ inline volatile ("bl %0" :: "r" (r));
}

and calling it with

@c(_start) @section(".text.boot")
func _start() -> Never {
	set_sp(ImageLayout.topOfStack)
	bl(_main)
	hang()
}

@c @inline(never)
func _main() {
	print("Hello, World!")
}

but I get the following assembly:

                     EntryPoint:
0000000000080000         nop
0000000000080004         adr        x8, #0xa0830
0000000000080008         mov        sp, x8
000000000008000c         nop
0000000000080010         adr        x8, #0x800f4

                     loc_80014:
0000000000080014         wfi                                                    ; CODE XREF=EntryPoint+24
0000000000080018         b          loc_80014

I can see that adr x8, #0x800f4 is half of what I want, it's indeed the address of _main but it seems the actual bl instruction disappeared.
Am I missing something? I'm guessing the problem is in my bl C function.

Calling _main directly in Swift results in an added stp fp, lr, [sp, #-0x10]! at the beginning of _start which causes the processor to exception-loop. I solved it for the Pi by jumping across execution levels but I'd like to keep this as minimal as possible to make it a template for people to start with.

@convention doesn’t work on declarations, right? @convention(naked) would be IMO a natural way to write this, and since it’s part of the type system, most requirements can fall out of it correctly: represented as a plain code address, cannot accept arguments safely, cannot return safely, and cannot accidentally convert to a function that supports these things.

The awkward part is that a naked function (as described above) can neither be implemented nor called safely. Since Swift doesn’t expose machine registers, it’s also difficult to write a useful naked function in the first place. I think it’s probably not enough to get you out of writing assembly.

1 Like

It can be called safely but cannot have arguments and the only possible return values are Void (it returns, call with bl) and Never (it won’t return, call with b).

In my case I don’t need to call it, it could just be a future direction.