Is it possible to make the linker put a variable at a specific location?

As many of you know, I maintain ATTinyCore.

It has bootloaders based on Optiboot. There has been a longstanding problem which I finally decided to come clean about on Github and opened an issue for. Basically, virtual boot (the trick used to get bootloader support on parts without hardware bootloader support) relies on rewriting the vector table as it programs the part. This works great - except for one situation: If the programming is interrupted right at the beginning, when it has erased the first page of flash, but not rewritten it. Then, the modified vector table will be gone, and neither the old sketch, nor the bootloader can run. While this situation sounds unlikely, it has occurred more than once for me.

Anyway - I had an idea to fix this: If the first instruction word of the second page of flash was a JMP (or RJMP, as appropriate for the part in question) pointed at the bootloader, when this happened, execution would just skid along the erased flash (0xFFFF is a no-op), hit the JMP/RJMP, and land in the bootloader so it could be reprogrammed, and all would be well again.

So my question to those who are experts with bullying the linker into doing weird things - how can I make the linker put a specific value into the flash at the start of the second page. This is an address typically a few bytes after the end of the vector table. Does anyone have any thoughts here? I haven't dug this deep into the compiler, so I'm not really sure where to start. Anything that leads me closer to a solution would be awesome.

Here's an assembly listing of one relevant part (a 1634, which has 32-byte pages, but does 4-page erase. Thus, this special JMP instruction needs to go at 0x80 (byte address). I am willing to sacrifice the space between the end of the vector table and 0x80.

Disassembly of section .text:

00000000 <__vectors>:
__vectors():
   0:	0c 94 50 00 	jmp	0xa0	; 0xa0 <__ctors_end>
   4:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
   8:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
   c:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  10:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  14:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  18:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  1c:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  20:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  24:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  28:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  2c:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  30:	0c 94 ee 00 	jmp	0x1dc	; 0x1dc <__vector_12>
  34:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  38:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  3c:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  40:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  44:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  48:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  4c:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  50:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  54:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  58:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  5c:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  60:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  64:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  68:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>
  6c:	0c 94 62 00 	jmp	0xc4	; 0xc4 <__bad_interrupt>

00000070 <__trampolines_end>:
__trampolines_start():
  70:	00 30       	cpi	r16, 0x00	; 0
  72:	2c 28       	or	r2, r12

00000074 <port_to_pullup_PGM>:
  74:	00 32 2e 2a                                         .2.*

00000078 <port_to_output_PGM>:
  78:	00 31 2d 29                                         .1-)

0000007c <digital_pin_to_port_PGM>:
  7c:	02 01 01 01 01 01 01 01 01 03 03 03 03 03 02 02     ................
  8c:	02 03                                               ..

(note that this does not contain the modified reset vector, since it's an assembly listing - the reset vector gets modified by the bootloader as it's uploaded)

ie, what I want to do is to tell the linker to put the stuff that starts at 0x70 20 bytes later, at 0x84, and put my 4-byte rescue-JMP at 0x80... If the vector table and the code weren't in the same section, I think this would be reasonably easy (a few section-start parameters, with the magic JMP in a section starting at 0x80 and the code starting at 0x84) - but I don't know how to get them into separate sections so I can do this...

On the upside - if I'm understanding all this correctly, it shouldn't even require bootloader changes, just some ugly changes to the core...

I just briefly read your post, because I have not much time, but probably you are looking for memory sections:
https://www.nongnu.org/avr-libc/user-manual/mem_sections.html

If you look into old optiboot source v5.0 I think, there is something like this:

	asm(".section .version                                                        \n"
		"    optiboot_version: .word " MAKEVER(OPTIBOOT_MAJVER, OPTIBOOT_MINVER) "\n"
		".section .text                                                           \n");

and then this param is used:
-Wl,--section-start=.version=0x7ffe

so the last 2 bytes of flash mem are set to optoboot version.

...or here:

unsigned int __attribute__((section(".myvariable"))) myVar = 0;

Yeah, I've seen that - but both the vectors and the rest of the sketch are in .text - and I need to insert my rescue-JMP in between those two things... that's what I'm stuck on...

You can put a variable (or a bit of code) at a specific location by using an attribute to put it in a specific section, and then using --section-start linker switch (or presumably a linker script) to put that section at a specific address. Optiboot, for example, does this for the version number.
Optiboot.c:

unsigned const int __attribute__((section(".version"))) 
optiboot_version = 256*(OPTIBOOT_MAJVER + OPTIBOOT_CUSTOMVER) + OPTIBOOT_MINVER;

Link command:

avr-gcc -g -Wall -Os -fno-split-wide-types -mrelax -mmcu=atmega328p -DF_CPU=16000000L  -DBAUD_RATE=115200 -DLED_START_FLASHES=3          -Wl,--section-start=.text=0x7e00 [b]-Wl,--section-start=.version=0x7ffe[/b] -Wl,--relax -nostartfiles -o optiboot_atmega328.elf optiboot.o

But doesn't your scheme require that

  • the 2nd page (page == "smallest erased block") be written before the first page (containing the vectors)
  • The total size of the vectors be smaller than the smallest erased block.

It almost seems like you could do this during programming with no modifications to either sketch or bootloader. Just have a sort of "pre-image" upload that re-programs the 2nd page with nothing but jump instructions. Since it wouldn't contain the vector page, it shouldn't cause the vector page re-write.