MSP430 Rust and C interop ELF size is just 6 bytes

I'm trying to create a proof of concept for Rust and C interoperability on msp430g2553 platform. The idea is Rust code calls a rudimentary UART library to send text through COM to the computer. My app compiles and links seemingly fine, but when I check with msp430-elf-size utility:

   text    data     bss     dec     hex filename
      6       0       4      10       a msp430-c-interop

And mspdebug utility flashes exactly 6 bytes to the device. The actual file size is 1952 bytes.

msp430-elf-objdump with disassembly prints:

msp430-c-interop:     file format elf32-msp430 


Disassembly of section .text: 

0000c000 <\__crt0_start>: 
    c000:       31 40 00 04     mov     #1024,  r1      ;#0x0400

Seems like it enters __crt0_start implementation from the C library and then probably can't find the main function and resets the device continuously.

I tried removing the msp430-rt crate's entry implementation and decorate the Rust main with extern "C" main() -> ! but that didn't work either.

I created a repo on GitHub. The code can be found there: GitHub - burumdev/msp430-rs-c-interop: Experimental and mental app to demonstrate Rust and C interop in msp430 embedded environment · GitHub

I noticed you are using a linker script that is likely provided by the C toolchain. rust applications using the rt crate does not use the gnu linker script, the rt crate provides its own linker script named link.x. it is documented in the "an example" section, which I think should have been moved to some more obvious places, especially for people who's new to the rust embedded ecosystem. I quote:

To actually build this program you need to place a memory.x linker script somewhere the linker can find it, e.g. in the current directory; and then link the program using msp430-rt’s linker script: link.x. The required steps are shown below:
...

if you are not familiar with convention for embedded rust, e.g. the concept of hal, pac and rt crates, you can use a template project as a reference, such as this:

Thank you for the reply. I tried to find a solution by simplifying and applying build configuration from the quickstart, but I'm now getting a cryptic linker error:

error: linking with `msp430-elf-gcc` failed: exit status: 1

libuart.a: error adding symbols: file format not recognized
          collect2: error: ld returned 1 exit status

I asked LLMs and stuff but, totally out of ideas :100:

I think maybe the C code is not built properly, e.g. the cross compiler may be not detected automatically.

you can check the elf header of the library, e.g. with readelf -h path/to/libuart.a, and you should see something like this:

$ readelf -h libhello.a
File: libhello.a(hello.o)
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          456 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         12
  Section header string table index: 11

these are the output on my linux laptop. pay speical attention to fields like OS/ABI and Machine, it should show msp430 specific info, although I don't what is the exact output for an msp430 target.

if cc failed to pick the correct cross compilation toolchain, you can manually specify it via the builder.compiler() and builder.archiver() API, or set the environment variable CC and AR accordingly.

Thanks a lot. I got it working now.

It turned out exactly as you pointed out. CC was unable to find TI's GCC compiler so it was spitting out AMD64 libuart.a . msp430 won't be able to run that in the near future. So .compiler() and .archiver() was necessary in my case. Might not be necessary if the binaries are in path, but I didn't try that.

With optimizations the final release firmware size on flash is 384 bytes. I think it's quite acceptable. With debug symbols the firmware grows to 484 bytes. With that 100 bytes I can debug Rust side of the firmware, but for some reason I can't step into C functions. Pressing 's' just skips over the function call. I guess it's something I'll need to figure out.

And minicom shows the results:

:man_climbing:

I don't think $PATH is the cause.

cc use a list of known target triples to decide the default toolchain prefix for cross compilation, see the prefix_for_target() method:

msp430 is not in the list (yet), I think it's simply because it is a relatively uncommon target. you may open a request or send a patch to add msp430 to the list.

btw, looking at the code in prefix_for_target(), it first checks whether the environment variable CROSS_COMPILE is set, which is a well established convention in many cross compiler toolchain. this way you don't have to set the individual commands like CC and AR, it is sufficient to set a common prefix.

maybe the C code does not have debug symbols or line mappings. what CFLAGS did you set?

I don't know why debug build profile is not reflected on CC's side. Based on the documentation here:

It should emit debug symbols. My Cargo.toml relevant section is:

[profile.dev]
opt-level = 1       # Light optimization: reduces binary size so it fits on flash, but keeps variables debuggable.
debug = true        # Generates full debug symbols for GDB/probe-rs.
lto = false         # Keep link-time optimization off for faster compilation.

[profile.release]
opt-level = "z"     # Optimize aggressively for size, disabling loop vectorization
strip = true        # Automatically strip symbols and debug info from the binary
lto = true          # Enable Link-Time Optimization across all crates
codegen-units = 1   # Reduce parallel code generation units to maximize optimizations
panic = "abort"     # Remove costly stack unwinding code, aborting on panic immediately

I also tried with the builder.debug(true) CC builder api method and it didn't work either.
I can step into Rust calls, can't step into C calls, can do si method to move instruction by instruction and I can see the value passed to the C function (the byte to uart_putchar for example), but the C code is not there when I list or enable tui.

yes, it should.

just to double check, did you see sections with names starting with .debug, such as .debug_info, .debug_line? here's an example on my machine:

$ readelf -S libhello.a 

File: libhello.a(hello.o)
There are 21 section headers, starting at offset 0x568:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000000b  0000000000000000  AX       0     0     1
  [ 2] .data             PROGBITS         0000000000000000  0000004b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 3] .bss              NOBITS           0000000000000000  0000004b
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .debug_info       PROGBITS         0000000000000000  00000050
       000000000000004b  0000000000000000   C       0     0     8
  [ 5] .rela.debug_info  RELA             0000000000000000  00000338
       00000000000000c0  0000000000000018   I      18     4     8
  [ 6] .debug_abbrev     PROGBITS         0000000000000000  0000009b
       0000000000000040  0000000000000000           0     0     1
  [ 7] .debug_aranges    PROGBITS         0000000000000000  000000e0
       000000000000002f  0000000000000000   C       0     0     8
  [ 8] .rela.debug_[...] RELA             0000000000000000  000003f8
       0000000000000030  0000000000000018   I      18     7     8
  [ 9] .debug_line       PROGBITS         0000000000000000  0000010f
       000000000000004f  0000000000000000           0     0     1
  [10] .rela.debug_line  RELA             0000000000000000  00000428
       0000000000000060  0000000000000018   I      18     9     8
  [11] .debug_str        PROGBITS         0000000000000000  0000015e
       000000000000003e  0000000000000001  MS       0     0     1
  [12] .debug_line_str   PROGBITS         0000000000000000  0000019c
       0000000000000022  0000000000000001  MS       0     0     1
  [13] .comment          PROGBITS         0000000000000000  000001be
       0000000000000023  0000000000000001  MS       0     0     1
  [14] .note.GNU-stack   PROGBITS         0000000000000000  000001e1
       0000000000000000  0000000000000000           0     0     1
  [15] .note.gnu.pr[...] NOTE             0000000000000000  000001e8
       0000000000000030  0000000000000000   A       0     0     8
  [16] .eh_frame         PROGBITS         0000000000000000  00000218
       0000000000000038  0000000000000000   A       0     0     8
  [17] .rela.eh_frame    RELA             0000000000000000  00000488
       0000000000000018  0000000000000018   I      18    16     8
  [18] .symtab           SYMTAB           0000000000000000  00000250
       00000000000000d8  0000000000000018          19     8     8
  [19] .strtab           STRTAB           0000000000000000  00000328
       000000000000000f  0000000000000000           0     0     1
  [20] .shstrtab         STRTAB           0000000000000000  000004a0
       00000000000000c6  0000000000000000           0     0     1

the .debug_line section contains line mappings, which is essential to single step the program in a debugger, and you can check the content like this:

$ readelf --debug-dump=decodedline libhello.a 

File: libhello.a(hello.o)
Contents of the .debug_line section:

hello.c:
File name                        Line number    Starting address    View    Stmt
hello.c                                    1                   0               x
hello.c                                    2                 0x4               x
hello.c                                    3                 0x9               x
hello.c                                    -                 0xb

if the debug information is indeed missing, try to rebuild with the environment varialbe CC_ENABLE_DEBUG_OUTPUT set to 1, it will print the full command line arguments of gcc invocation.

note: the -vv flag for cargo is required, and the output is quite verbose, pay attention to the line saying running: ..., for example:

$ cargo clean
$ CC_ENABLE_DEBUG_OUTPUT=1 cargo -vv build
...
[hello 0.1.0] running: LC_ALL="C" "cc" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-g" "-gdwarf-4" "-fno-omit-frame-pointer" "-m64" "-Wall" "-Wextra" "-o" "/tmp/cargo-build/debug/build/hello-3ed4458d5e107cd7/out/c6aeacfef515cb42-hello.o" "-c" "/tmp/hello.c"
...

for debug builds, you should see the -g flag. if the flag is missing, you should fire a bug report to cc.

Debug sections in the libuart.a file are full of debug info. And the GCC command that CC generates seems to be OK too:

\[msp430-c-interop 0.1.0\] running: LC_ALL="C" "/opt/ti/msp430-gcc/bin/msp430-elf-gcc" "-O1" "-ffunction-
sections" "-fdata-sections" "-g" "-fno-omit-frame-pointer" "-I" "src/c_drivers" "-I" "/opt/ti/msp430-gc
c/include" "-Wall" "-Wextra" "-mmcu=msp430g2553" "-o" ......

-g flag is there.

Archiver command is like the following:
\[msp430-c-interop 0.1.0\] running: ZERO_AR_DATE="1" "/opt/ti/msp430-gcc/bin/msp430-elf-gcc-ar" "cqD" PATH/XXX-uart.o
and following:

running: "/opt/ti/msp430-gcc/bin/msp430-elf-gcc-ar" "sD" PATH/libuart.a`

One thing I noticed is that there's a stray gcc run with -? option that goes to nowhere before the successful run:

\[msp430-c-interop 0.1.0\] running: "/opt/ti/msp430-gcc/bin/msp430-elf-gcc" "-?" 
\[msp430-c-interop 0.1.0\] cargo:warning=msp430-elf-gcc: error: unrecognized command line option '-?' 
\[msp430-c-interop 0.1.0\] cargo:warning=msp430-elf-gcc: fatal error: no input files 
\[msp430-c-interop 0.1.0\] cargo:warning=compilation terminated. 
\[msp430-c-interop 0.1.0\] exit status: 1

Interesting indeed. Is it possible that the linker or the archiver strips these symbols before generating the ELF?

EDIT: Seems not like that. As I can see the C debug_line addresses in the generated firmware in addition to Rust debug_lines:

readelf --debug-dump=decodedline msp430-c-interop
 
src/main.rs: 
main.rs                                   21              0xc036               x 
main.rs                                   27              0xc03a               x 
main.rs                                   33              0xc046               x 
main.rs                                   34              0xc04e               x 
main.rs                                   35              0xc056               x 
main.rs                                   38              0xc05e               x 
main.rs                                   28              0xc060               x 
main.rs                                    -              0xc070 


readelf: Warning: The length field (0xfffffe5e) in the debug_line header is wrong - the section is too 
small 
Contents of the .debug_line.text.initUART section: 

<unknown>                                 52              0xc070               x 
<unknown>                                 53              0xc070       1       x 
<unknown>                                 53              0xc070       2 
<unknown>                                 54              0xc076       3       x 
<unknown>                                 54              0xc076       4 
<unknown>                                 55              0xc07c       5       x 
<unknown>                                 55              0xc07c       6 
<unknown>                                 56              0xc080       7       x 
<unknown>                                 56              0xc080       8 
<unknown>                                 57              0xc086       9       x 
<unknown>                                 57              0xc086      10 
<unknown>                                 58              0xc08a      11 
<unknown>                                  -              0xc08c 


Contents of the .debug_line.text.initClockTo16MHz section: 

<unknown>                                 65              0xc08c               x 
<unknown>                                 66              0xc08c       1       x 
<unknown>                                 66              0xc08c       2 
<unknown>                                 70              0xc094       3       x 
<unknown>                                 70              0xc094       4 
<unknown>                                 71              0xc098       5       x 
<unknown>                                 71              0xc098       6 
<unknown>                                 72              0xc09e       7       x 
<unknown>                                 72              0xc09e       8 
<unknown>                                 73              0xc0a4       9 
<unknown>                                 68              0xc0a6      10       x 
<unknown>                                 68              0xc0a6      11       x 
<unknown>                                 68              0xc0a6      12       x 
<unknown>                                  -              0xc0aa 


Contents of the .debug_line.text.initGPIO section: 

<unknown>                                 76              0xc0a8               x 
<unknown>                                 77              0xc0a8       1       x 
<unknown>                                 77              0xc0a8       2 
<unknown>                                 78              0xc0ae       3       x 
<unknown>                                 78              0xc0ae       4 
<unknown>                                 79              0xc0b4       5 
<unknown>                                  -              0xc0b6 


Contents of the .debug_line.text.uart_putchar section: 

<unknown>                                 86              0xc0b6               x 
<unknown>                                 87              0xc0ba       1       x 
<unknown>                                 87              0xc0ba       2       x 
<unknown>                                 87              0xc0ba       3       x 
<unknown>                                 88              0xc0c0       4       x 
<unknown>                                 88              0xc0c0       5 
<unknown>                                 89              0xc0c4       6 
<unknown>                                  -              0xc0c6 


Contents of the .debug_line.text.uart_puts section: 

<unknown>                                 91              0xc0c6               x 
<unknown>                                 92              0xc0c6       1       x 
<unknown>                                 92              0xc0c6       2 
<unknown>                                 93              0xc0ca       3       x 
<unknown>                                 93              0xc0ca       4 
<unknown>                                 93              0xc0cc       5 
<unknown>                                 94              0xc0d4       6       x 
<unknown>                                 94              0xc0d4       7       x 
<unknown>                                 96              0xc0da       8       x 
<unknown>                                 96              0xc0da       9 
<unknown>                                 98              0xc0de      10       x 
<unknown>                                 93              0xc0de      11       x 
<unknown>                                 93              0xc0de      12 
<unknown>                                 93              0xc0e0      13 
<unknown>                                101              0xc0e4      14 
<unknown>                                105              0xc0e6      15 
<unknown>                                104              0xc0e8      16 
<unknown>                                101              0xc0ec      17 
<unknown>                                  -              0xc0f2 


Contents of the .debug_line.text.uart_setup section: 

<unknown>                                108              0xc0f0               x 
<unknown>                                109              0xc0f0       1       x 
<unknown>                                110              0xc0f4       2       x 
<unknown>                                111              0xc0f8       3       x 
<unknown>                                112              0xc0fc       4 
<unknown>                                  -              0xc0fe

The readelf warning might be the clue:

readelf: Warning: The length field (0xfffffe5e) in the debug_line header is wrong - the section is too 
small

I believe that's part of the automatic compiler detection procedure.

to be honest, it's a bit of misleading to be presented as cargo::warning, but cargo only support cargo::warning and cargo::error for build scripts to log custom messages, you can't print arbitrary messages in a build script because cargo captures the output of build scripts as an IPC channel between the scripts and cargo itself, so that's that.

that certainly looks like the culprit.

from the output of readelf you posted, clearly gcc emits the line mapping into individual sections per function, as indicated by the section names .debug_line.text.XXX where XXX is the function name.

this is ok in its own, and readelf also recognizes them, but the debugger expects a single .debug_line section, which, in your case, only contains the mappings for rust source files. this explains the behavior you observed: single stepping works fine in rust code but not in C code.

I think the linker script provided by the rt crate did not accommodate the way how the msp430 C compiler generates these debug info. it might be a msp430-gcc specific thing, as I don't remember seeing similar issue for cortex-m based microcontrollers.

try to add a directive in the linker script to combine all the line mappings into a single section, something like this in your memory.x should work, I think:

SECTIONS {
    .debug_line 0 : {
        *(.debug_line .debug_line.* .debug_line_end)
    }
}

note, this example only merges the .debug_line section, so you should be able to single step the C code in a debugger, but other debug info is not merged, so features like watch window and local variables might not work properly. check the stock linker script in the toolchain to see how they merge all the debug sections.

if this does not work, you can turn off the -ffunction-sections flag in the first place, which I believe is one of the default flags enabled by cc, so you'll need to disable the default flags all together, using either the builder api or a build time environment variable, see:

Adding the flag -fno-function-sections fixed the problem for me. I did that instead of disabling all flags and reinstating them. On the C side, now I can step into functions and print variables as well as on the Rust side.

It's good to know cortex-m based MCUs work better because my next POC target will be an mspm0 or stm32 board. And it's totally understandable they work better because they're widely available, more popular and the toolchains are more modern. Thank you very much. I'll have a lot of things to review and dive deeper into after this thread! :tada:

glad it works.

I didn't suggest this option because I wasn't sure how cc would order the contradicting flags. now that I think about it, it makes sense to put the default flags before any user specified flags, since they are, well, the default flags, after all. :sweat_smile:

well, actually I don't know if they wok "better" in this regard, I rarely need to step into ffi code in a debugger, if at all.

in fact, I don't think I ever used mixed langauges (C and rust) for a bare metal project. I did use ffi occasionally, most of which were needed for vendor proprietary libraries or third party libraries, but none of them required me to use a debugger to step into the ffi calls.