5

I'm trying to write a bootloader for an STM32 in Rust and I can't figure out how to correctly populate the stack pointer. Near as I can tell the code should be:

asm!("MOV SP, $0" :: "0"(stack_pointer));  // set the stack pointer

but the compiler disagrees:

error: invalid operand in inline asm: 'MOV SP, $0'
  --> src/main.rs:38:5
   |
38 |     asm!("MOV SP, $0" :: "0"(stack_pointer));  // set the stack pointer
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: <inline asm>:1:11: error: unexpected token in operand
        MOV SP, 
                ^

  --> src/main.rs:38:5
   |
38 |     asm!("MOV SP, $0" :: "0"(stack_pointer));  // set the stack pointer
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

What am I doing wrong? It seem to be complaining about the dollar sign, but I got that directly from the documentation.

Per the conversation in the comments, I've tried two things, both of which compile(!) but neither of which seem to work (but that could be for any of a billion reasons, still working on it):

Version A:

asm!("MOV R0, #0x0800");
asm!("LSL R0, R0, #16");
asm!("MOV R1, #0x8000");
asm!("ORR R2, R1, R0");
asm!("LDRT R0, [R2]");
asm!("MOV SP, R0");
entry_point()

Version B:

#[inline(never)]
unsafe fn go(_addr: u32, entry_point: fn()->()) {
    asm!("MOV SP, R0");
    entry_point()
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
teryret
  • 240
  • 2
  • 11
  • `$0` looks like AT&T x86 syntax - and in the link there are other examples that use Intel-like syntax (so just `0`) – UnholySheep Feb 23 '18 at 22:20
  • Replacing it with just zero changes the compiler error to one that suggests that valid values are [r0, r15] (the CPU registers for this chip (of which R13 == SP)) – teryret Feb 23 '18 at 22:21
  • Looking at the [reference for ARM `MOV`](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0068b/BABJAJIB.html) suggests that the correct syntax should be `MOV SP, #0` – UnholySheep Feb 23 '18 at 22:23
  • http://www.st.com/content/ccc/resource/technical/document/programming_manual/6c/3a/cb/e7/e4/ea/44/9b/DM00046982.pdf/files/DM00046982.pdf/jcr:content/translations/en.DM00046982.pdf seems to suggest you can use either SP or R13 – teryret Feb 23 '18 at 22:23
  • @UnholySheep #0 gives the same error as regular zero; "operand must be a register in range [r0, r15]" – teryret Feb 23 '18 at 22:24
  • 1
    The reference you linked states *"You can use SP and PC only in the MOV instruction, with the following restrictions: The second operand must be a register without shift; You must not specify the S suffix"* - so you cannot use `MOV SP` with an immediate value – UnholySheep Feb 23 '18 at 22:26
  • I don't think that I am doing that; the value of stack_pointer isn't known until runtime... unless maybe the asm! macro is filling in a memory address? But if that was the case why complain about the $ token? – teryret Feb 23 '18 at 22:28
  • @teryret *"The second operand must be a register without shift"* <-- $0 is not a register is it? Maybe that's what the compiler tells you: *"operand must be a register in range [r0, r15]"*. I would try MOV'ing $0 or whatever, into a register and then into SP, using two instructions. Something like `MOV R1, #0` `MOV SP, R1` – Morten Jensen Feb 23 '18 at 22:34
  • Ok, but isn't that begging the question? If I replace SP with R6 the same thing happens... how do I get something to cross the threshold from rust local variable to value in register? – teryret Feb 23 '18 at 22:36
  • I can simplify it even more, `asm!("MOV R0, #0x08008000");` doesn't build, and that doesn't involve any string interpolation nonsense. Edit: oh wait, nm, the value is too large to be a literal – teryret Feb 23 '18 at 22:39
  • My example was really bad, I see that now. I do think that is your problem however. That you need to get the variable into a register and then `MOV SP, Rx`. I don't know enough about Rust inline ASM to be able to help with getting access to Rust variables in the inline ASM. In C you can cheat and put the ASM code in a function and rely on the C calling convention on ARM, putting the arguments in R0, R1 etc. Don't know if that is any help. I guess not, since I wouldn't be comfortable manipulating SP in a call :( – Morten Jensen Feb 23 '18 at 22:42
  • I'm on board with that, just trying to figure out how to. I can get the first 16 bits of the address of the address into a register, just trying to figure out the right way to get the other 16 bits in there – teryret Feb 23 '18 at 22:43
  • 2
    Sorry for the comment-spam, I don't think I can help any more. I've upvoted your question because you've done a proper effort. Consider editing the question to reflect your current challenges. the compiler messages and insight from @UnholySheep. – Morten Jensen Feb 23 '18 at 22:50
  • Withe respect to the "billion reasons" it still may not work, does your bootloader disable all interrupts, correctly set the application's initial vector table and jump to the correct entry point that will establish the run-time environment expected by the application? The SP and PC should be assigned from the application's vectortable to emulate a direct start-up of the application - see https://stackoverflow.com/a/14406706/168986 - I have not offered an answer because I do not know Rust. – Clifford Feb 25 '18 at 08:37

2 Answers2

3

It seems to me you're having two problems:

  1. figuring out what the assembly code should look like in the end
  2. how to build it from Rust.

The second part seems easy with global_asm!:

#![feature(global_asm)]

global_asm!(r#"
.global go
go:
    mov sp, r0
    bx r1
"#);
extern "C" {
    pub fn go(addr: u32, entry_point: extern fn());
}

or with normal asm!:

#![feature(asm)]

#[no_mangle]
pub unsafe fn go(addr: u32, entry_point: fn()) {
    asm!("mov sp, r0" :: "{r0}"(addr) : "sp");
    entry_point()
}

I think the "0"(stack_pointer) part didn't work because stack_pointer is not a constant.

Your Version B results in the same ASM code; but it really should mark "sp" as clobbered, and "{r0}"(addr) makes sure the first argument really is in r0.

Clifford seems to prefer passing a pointer to a "vector table", which could look like this:

#[repr(C)]
pub struct VectorTable {
    stack_pointer: u32,
    entry_point: extern fn() -> !,
}

global_asm!(r#"
.global go
go:
    ldr sp, [r0]
    ldr pc, [r0, #4]
"#);
extern "C" {
    pub fn go(vt: &VectorTable) -> !;
}
Stefan
  • 5,304
  • 2
  • 25
  • 44
  • 1
    I don't know Rust, so cannot answer but I know STM32 (and Cortex-M in general), and tools normally generate a vector table with SP and Entry point in the first two words. Jumping to the entry-point by a call after setting the SP would allow you to return to the bootloader, which may not be wanted. It may be better to load the PC directly in assembler too - as I have in https://stackoverflow.com/a/14406706/168986 (in C/Assembler) using only the vector table data. It ensures that the run-time environment is re-esablished as defined by the application build rather than the bootloader. – Clifford Feb 25 '18 at 08:39
  • @Clifford At least in release mode the call to entry_point is optimized to a tail call (on my system); i.e. it actually just branches (`bx`), same as my `global_asm!` variant. It would probably be better to use `entry_point: fn() -> !` (but then it doesn't use tail call optimization...). – Stefan Feb 25 '18 at 11:41
  • 1
    I question however where the value for entry point comes from. In any particular application build it may vary, whereas the initial vector table is at the base address specified to the linker. So what I am suggesting is to take both that SP and PC from this known location. You can then boot any application built in any toolchains given the correct base address. – Clifford Feb 25 '18 at 12:54
  • Well, whether you extract SP and PC from the vector table and pass it to `go` or pass the address to (SP, PC) to `go` is not a big difference imho. Where those addresses come from might still be the OPs main problem... – Stefan Feb 25 '18 at 12:59
  • 1
    I don't disagree - but the application is built independently of the bootloader, possibly in a different source language and toolchain, so using the vector table ensures the application is started up as the application build expects. It may be the case that the OP is indeed doing that, but if so you do not need two addresses. My comment was not a criticism of this answer, more information for the OP in case the possibility had not occurred. – Clifford Feb 25 '18 at 19:31
0

I ended up going with @stefan's implementation of @clifford's approach along with the following code to call it:

let firmware_address = 0x08008000u32;
unsafe {
    let scb = &*stm32f40x::SCB::ptr();
    scb.vtor.write(firmware_address);
    asm::isb();
    go(&*(firmware_address as *const VectorTable));
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366