riscv-non-isa/riscv-toolchain-conventions

medlow code generation

Closed this issue · 13 comments

I am trying to understand and explain mcmodel=medlow behavior. I see that it is supposed to generate addresses with lui/addi and that there is a proposal to deprecate.

I tried comparing -mcmodel= medlow and medany with a code snippet for la.

.section .text.init
.globl rvtest_entry_point
rvtest_entry_point:
    la t0, Y
    lw t1, 0(t0)
 
.align 20
X:
.word 0x00000000
Y:
.word 0x00000000

The assembler produced the same code for both for -march=rv64gc.

    80000000:	00100297          	auipc	t0,0x100
    80000004:	00428293          	add	t0,t0,4 # 80100004 <Y>
    80000008:	0002a303          	lw	t1,0(t0)

I would have expected medlow to generate
lui t0, 0x80100
addi t0, t0, 0x4

Can you tell me if medlow is becoming the same as medany or what is happening here?

Thank you!

@TommyMurphyTM1234

I have looked at these references but am wondering if they are outdated.

The scanty proposal is on this repo:
https://github.com/riscv-non-isa/riscv-toolchain-conventions

'''
Issues for consideration

It has been proposed to deprecate the medlow code model and rename medany to medium.
'''

Also, on this repo, medlow is supposed to use lui, while medany is supposed to use auipc. In my experiment with a recent GCC, both produce auipc for la. I'm wondering if medlow is now handled like medany in all cases, or if there's something funny about my experiment.

'''
-mcmodel=medlow. The program and its statically defined symbols must lie within a single 2GiB address range, between the absolute addresses -2GiB and +2GiB. lui and addi pairs are used to generate addresses.

-mcmodel=medany. The program and its statically defined symbols must lie within a single 4GiB address range. auipc and addi pairs are used to generate addresses.

'''

I have looked at these references but am wondering if they are outdated.

On what basis are you wondering that?

Maybe @kito-cheng or @cmuellner might be able to comment on this issue?

I am trying to understand and explain mcmodel=medlow behavior. I see that it is supposed to generate addresses with lui/addi and that there is a proposal to deprecate.

I tried comparing -mcmodel= medlow and medany with a code snippet for la.

.section .text.init
.globl rvtest_entry_point
rvtest_entry_point:
    la t0, Y
    lw t1, 0(t0)
 
.align 20
X:
.word 0x00000000
Y:
.word 0x00000000

The assembler produced the same code for both for -march=rv64gc.

    80000000:	00100297          	auipc	t0,0x100
    80000004:	00428293          	add	t0,t0,4 # 80100004 <Y>
    80000008:	0002a303          	lw	t1,0(t0)

The code model is primarily relevant for compilers and linkers, not the assembler.
If you input hand-written assembly routines into an assembler, don't expect them to be modified.

la is a pseudo-instruction, and its definition can be looked up in the RISC-V Assembly Programmer's Manual:
la rd, symbol becomes auipc rd, symbol[31:12]; addi rd, rd, symbol[11:0]. And that's exactly what you are observing.

I would have expected medlow to generate lui t0, 0x80100 addi t0, t0, 0x4

The code models are defined in the RISC-V ELF psABI specification.
This specification has an ELF section that defines the properties of the code models, among other details.

I'm not sure what you want to achieve, but I made up this example in C to demonstrate the effect of the code models:

long X;
long Y;

long getX(void)
{
	return X;
}

long getY(void)
{
	return Y;
}

I've compiled this as follows:

  • medlow: riscv64-unknown-linux-gnu-gcc -O3 -march=rv64gc -mcmodel=medlow -c loadval.c -o loadval_medlow.o
  • medany: riscv64-unknown-linux-gnu-gcc -O3 -march=rv64gc -mcmodel=medany -c loadval.c -o loadval_medany.o

Now, let's inspect the generated code (incl. the relocations) for medlow:

$ riscv64-linux-gnu-objdump -d -r loadval_medlow.o

loadval_medlow.o:     file format elf64-littleriscv


Disassembly of section .text:

0000000000000000 <getX>:
   0:	000007b7          	lui	a5,0x0
			0: R_RISCV_HI20	X
			0: R_RISCV_RELAX	*ABS*
   4:	0007b503          	ld	a0,0(a5) # 0 <getX>
			4: R_RISCV_LO12_I	X
			4: R_RISCV_RELAX	*ABS*
   8:	8082                	ret

000000000000000a <getY>:
   a:	000007b7          	lui	a5,0x0
			a: R_RISCV_HI20	Y
			a: R_RISCV_RELAX	*ABS*
   e:	0007b503          	ld	a0,0(a5) # 0 <getX>
			e: R_RISCV_LO12_I	Y
			e: R_RISCV_RELAX	*ABS*
  12:	8082                	ret

Let's compare this with the definition of medlow:

    # Load value from a symbol
    lui  a0, %hi(symbol)
    lw   a0, %lo(symbol)(a0)

Matches just fine!

Now, let's inspect the generated code (incl. the relocations) for medany:

$ riscv64-linux-gnu-objdump -d -r loadval_medany.o

loadval_medany.o:     file format elf64-littleriscv


Disassembly of section .text:

0000000000000000 <getX>:
   0:	00000517          	auipc	a0,0x0
			0: R_RISCV_PCREL_HI20	X
			0: R_RISCV_RELAX	*ABS*
   4:	00053503          	ld	a0,0(a0) # 0 <getX>
			4: R_RISCV_PCREL_LO12_I	.L0 
			4: R_RISCV_RELAX	*ABS*
   8:	8082                	ret

000000000000000a <getY>:
   a:	00000517          	auipc	a0,0x0
			a: R_RISCV_PCREL_HI20	Y
			a: R_RISCV_RELAX	*ABS*
   e:	00053503          	ld	a0,0(a0) # a <getY>
			e: R_RISCV_PCREL_LO12_I	.L0 
			e: R_RISCV_RELAX	*ABS*
  12:	8082                	ret

Let's compare this with the definition of medany:

         # Load value from a symbol
.Ltmp0:  auipc a0, %pcrel_hi(symbol)
         lw    a0, %pcrel_lo(.Ltmp0)(a0)

Again, matches just fine.

HTH

I'm wondering if the references are outdated because the references say medlow generates addresses with lui+addi, while my experiment produces auipc for both medlow and medany, rather than lui.

In other words, my experiment is finding GCC produces relative addressing for both mcmodels, while the docs say medlow is absolute and medany is relative.

@cmuellner That explains what I'm seeing. Thank you very much.

Thanks @cmuellner for that useful clarifying info.
Would you happen to know if this is still being considered?

Issues for consideration

It has been proposed to deprecate the medlow code model and rename medany to medium.

I've created a PR to remove the misleading deprecation note (#44).

Having this note is wrong (even if the proposal is still being considered) because we don't discuss proposals in documents that users read.

@cmuellner Thank you for your wonderful example. I was able to reproduce it. However, the X and Y addresses have not been through the linker so their addresses are garbage.

I tried adding a main function to be able to compile a complete program and see how the memory models work with real addresses. This is the same as your program except that there is a main function that calls getX() and getY().

//loadvalmain.c

long X;
long Y;

long getX(void)
{
	return X;
}

long getY(void)
{
	return Y;
}

int main(void)
{
    return getX() + getY();
}

Now I compiled both:

riscv64-unknown-elf-gcc -O -march=rv64gc -mcmodel=medlow loadvalmain.c -o loadvalmain_medlow
riscv64-unknown-elf-objdump -d -r loadvalmain_medlow > loadvalmain_medlow.objdump
riscv64-unknown-elf-gcc -O -march=rv64gc -mcmodel=medany loadvalmain.c -o loadvalmain_medany
riscv64-unknown-elf-objdump -d -r loadvalmain_medany > loadvalmain_medany.objdump

The interesting excerpts are:


0000000000010116 <_start>:
   10116:	00002197          	auipc	gp,0x2
  1011a:	70a18193          	add	gp,gp,1802 # 12820 <__global_pointer$>
 ...
   10152:	bf59                	j	100e8 <exit>

00000000000101a8 <main>:
   101a8:	1a81b503          	ld	a0,424(gp) # 129c8 <X>
   101ac:	1a01b783          	ld	a5,416(gp) # 129c0 <Y>
   101b0:	9d3d                	addw	a0,a0,a5
   101b2:	8082                	ret

This code is the same in both medlow and medany. In both cases, the variables are accessed relative to the global pointer, and the global pointer is initialized with auipc relative to the PC, so the code is relocatable.

This is the behavior I expect for medany, but isn't the absolute addressing I would expect for medlow. When a program is fully linked, under what circumstances would medlow generate different code than medany?

Thank you,

David

@cmuellner Thank you for your wonderful example. I was able to reproduce it. However, the X and Y addresses have not been through the linker so their addresses are garbage.

I tried adding a main function to be able to compile a complete program and see how the memory models work with real addresses. This is the same as your program except that there is a main function that calls getX() and getY().

//loadvalmain.c

long X;
long Y;

long getX(void)
{
	return X;
}

long getY(void)
{
	return Y;
}

int main(void)
{
    return getX() + getY();
}

Now I compiled both:

riscv64-unknown-elf-gcc -O -march=rv64gc -mcmodel=medlow loadvalmain.c -o loadvalmain_medlow
riscv64-unknown-elf-objdump -d -r loadvalmain_medlow > loadvalmain_medlow.objdump
riscv64-unknown-elf-gcc -O -march=rv64gc -mcmodel=medany loadvalmain.c -o loadvalmain_medany
riscv64-unknown-elf-objdump -d -r loadvalmain_medany > loadvalmain_medany.objdump

The interesting excerpts are:


0000000000010116 <_start>:
   10116:	00002197          	auipc	gp,0x2
  1011a:	70a18193          	add	gp,gp,1802 # 12820 <__global_pointer$>
 ...
   10152:	bf59                	j	100e8 <exit>

00000000000101a8 <main>:
   101a8:	1a81b503          	ld	a0,424(gp) # 129c8 <X>
   101ac:	1a01b783          	ld	a5,416(gp) # 129c0 <Y>
   101b0:	9d3d                	addw	a0,a0,a5
   101b2:	8082                	ret

This code is the same in both medlow and medany. In both cases, the variables are accessed relative to the global pointer, and the global pointer is initialized with auipc relative to the PC, so the code is relocatable.

This is the behavior I expect for medany, but isn't the absolute addressing I would expect for medlow. When a program is fully linked, under what circumstances would medlow generate different code than medany?

The global pointer will be set up independently of the code model and the written code.
This can be shown by further simplifying main:

int main(void)
{
	return 42;
}

The disassembly now shows:

[...]
00000000000103f0 <main>:
   103f0:       02a00513                li      a0,42
   103f4:       8082                    ret
[...]
00000000000103f8 <_start>:
   103f8:       022000ef                jal     1041a <load_gp>
[...]
000000000001041a <load_gp>:
   1041a:       00002197                auipc   gp,0x2
   1041e:       3e618193                add     gp,gp,998 # 12800 <__global_pointer$>
   10422:       8082                    ret
[...]

So, in your example, the linker removed the lui in the medlow case and the auipc in the medany case (because it found that ld is sufficient to access the values). What remains are the ld instructions and the add.

Would tweaking the linker script to craft a memory map/layout that triggers different medany/medlow code generation/linkage be a way to see the differences?

Would tweaking the linker script to craft a memory map/layout that triggers different medany/medlow code generation/linkage be a way to see the differences?

Yes, but let's use a much simpler mechanism:

long X;
long dummy[100000];
long Y;

long getX(void)
{
        return X;
}

long getY(void)
{
        return Y;
}

int main(void)
{
        return getX() + getY();
}

medlow:

[...]
00000000000103f0 <main>:
   103f0:       8101b503                ld      a0,-2032(gp) # 12030 <X>
   103f4:       67c9                    lui     a5,0x12
   103f6:       0287b783                ld      a5,40(a5) # 12028 <Y>
   103fa:       9d3d                    addw    a0,a0,a5
   103fc:       8082                    ret
[...]

medany:

[...]
00000000000103f0 <main>:
   103f0:       8101b503                ld      a0,-2032(gp) # 12030 <X>
   103f4:       00002797                auipc   a5,0x2
   103f8:       c347b783                ld      a5,-972(a5) # 12028 <Y>
   103fc:       9d3d                    addw    a0,a0,a5
   103fe:       8082                    ret
[...]