RISC-V Startup: Initialize gp Without Relaxing Away Its Initialization

A bare-metal RISC-V image reaches startup but fails as soon as it accesses a small global object. The startup source contains a load of gp, yet the linked instructions no longer establish its value. Global-pointer relaxation can create that circular dependency if the initialization itself is eligible for relaxation.

Inspect the final startup sequence and the global-pointer contract together. This failure is not repaired by copying initialized data correctly while leaving gp undefined.

Understand what the linker is allowed to shorten

The pinned psABI global-pointer relaxation section describes replacing eligible symbol-address sequences with accesses relative to gp. It requires an appropriate x3 usage declaration and a symbol offset within the supported signed range, approximately two KiB on either side of the global pointer.

A two-instruction address calculation can therefore become a load or store that assumes gp already contains the address associated with __global_pointer$. That assumption must be established before the first such access, including accesses inside startup helpers.

The problematic case is using an ordinary relaxable address-loading sequence to establish gp itself. The same specification warns that it can become a no-operation-like mv gp, gp, which preserves whatever value happened to be present rather than loading the required address.

Protect only the initialization sequence

The official GNU assembler RISC-V directives give a concrete scoped pattern: .option push, .option norelax, la gp, __global_pointer$, then .option pop.

The push/pop pair matters because it restores the surrounding assembler options. Keeping relaxation disabled accidentally for the rest of a startup file can change unrelated code generation and complicate comparison with the intended build.

Place the protected initialization before any operation whose emitted code might use gp. Do not assume that an early C function is safe merely because its source looks trivial; inspect its actual global accesses and compiler-generated references. The sequence belongs in the entry path that really executes, not an unused startup file retained in the repository.

Diagram showing gp-relative linker relaxation, its circular startup hazard, protected initialization and final-image checks.
Original startup-relaxation diagram. It contains source-derived mechanisms and illustrative address reasoning, not a reproduced crash.

Compare relocations with the linked instructions

In the proposed diagnostic, inspect the startup object with a target-capable objdump -dr to see both instructions and relocations. Then inspect the final ELF at the actual entry point. Link-time changes are precisely why source code or an unlinked object alone is insufficient.

Resolve __global_pointer$ in the final symbol table and compare the initialized register value with that address in a controlled debugger session. Follow the first failing global access: determine whether its base is gp, which offset it uses and which address that combination produces.

For example, if a load should use a symbol at global-pointer offset 24, an uninitialized gp of zero directs it toward address 24 rather than the intended object. This is illustrative address arithmetic, not an observed crash. A valid data section at the intended location does not repair the wrong base register.

Confirm that x3 actually belongs to this ABI contract

The psABI calling-convention source reserves x3 as the global pointer under the standard convention. It also allows a platform to define another dedicated use, provided that use is documented and incompatible global-pointer relaxation is disabled.

Do not impose a conventional gp initialization on a platform that deliberately uses x3 for another purpose. Equally, do not repurpose x3 in an ordinary runtime while linked objects still depend on gp-relative accesses. Review the selected startup, libraries and platform ABI as one contract.

A useful acceptance check demonstrates that the executed initialization survives linking, its value matches the linker-defined symbol and representative gp-relative accesses reach the intended objects. No compiler, linker or target debugger was run for this article. The source mechanism explains what to inspect and why a seemingly present initialization can disappear.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *