|
|
Log in / Subscribe / Register

Function pointers are not always just a pointer to the instruction

Function pointers are not always just a pointer to the instruction

Posted Jul 16, 2025 19:28 UTC (Wed) by jrtc27 (subscriber, #107748)
Parent article: Following up on the Python JIT

> One caveat is that memory from mmap() comes in page-sized chunks, which is 4KB on most systems but can be larger. If the JIT code is, say, four bytes in length, that can be wasteful, so it needs to be managed carefully. Once you have that memory, he asked, how do you actually execute it? It turns out that "C lets us do crazy things":
>
> typedef int (*function)(int);
> ((function)data)(42);
>
> That first line creates a type definition named "function", which is a pointer to a function that takes an integer argument and returns an integer. The second line casts the data pointer to that type and then calls the function with an argument of 42 (and ignores the return value). "It's weird, but it works."

This isn't always true. Most of the time it is, but there are a couple of cases where it's not.

Firstly, on some architectures, low bits of the address for indirect jumps, and thus function pointers, are used to indicate which execution mode the processor should use. For example, on 32-bit Arm, the LSB is 1 for T32/Thumb and 0 for A32/Arm, and on 32-bit MIPS it similarly distinguishes between MIPS32 and microMIPS32 (or the prior MIPS16e which microMIPS32 replaced).

Secondly, some ABIs use function descriptors to represent language-level function pointers. Here, the function pointer is not a pointer to the instructions to execute but is a pointer to a structure that contains such a pointer alongside one or more other pointers, typically some kind of per-library global pointer. This is the case on PA-RISC, Itanium, and 64-bit PowerPC if using version 1 of its ELF ABI (version 2 drops this, and is what most modern distributions use for 64-bit PowerPC), but also in embedded contexts multiple other ISAs have an ABI variant (sometimes called "FDPIC" for "function descriptor position-independent code") that does so, since it allows you to share a single copy of library code between processes in no-MMU systems.


to post comments

Function pointers are not always just a pointer to the instruction

Posted Jul 22, 2025 21:31 UTC (Tue) by anton (subscriber, #25547) [Link] (4 responses)

Your post makes me appreciate our use goto * (instead of C function calls) to enter generated machine code in gforth.

As for ARM, we have used the address of the first byte of the code as target, and that has worked whether the code used T32 or A32. Are you sure that the mode stuff applies to T32 (Thumb2) and not just to Thumb1?

Function pointers are not always just a pointer to the instruction

Posted Jul 23, 2025 9:45 UTC (Wed) by farnz (subscriber, #17727) [Link]

The mode stuff on ARM applies to all processors that have both T32 and A32 modes. See the BX instruction. If you're jumping to a register (absolute address), not to an inline offset, the bottom bit determines whether the destination is T32 or A32 code.

This happens to not be a problem for jumps to intentionally generated machine code, because T32 instructions must be 16-bit aligned, and A32 instructions must be 32-bit aligned.

Function pointers are not always just a pointer to the instruction

Posted Jul 23, 2025 10:01 UTC (Wed) by excors (subscriber, #95769) [Link] (2 responses)

Thumb-2 is the same. When you use an interworking branch instruction (`bx`, `blx`, `ldr pc`, `pop {pc}`, etc), the LSB determines whether the CPU switches into A32 or T32 mode. When you use non-interworking branches (`b`, `bl`, `mov pc`, etc), the CPU remains in its current mode, and the bottom 1-2 LSBs of the address are replaced with 0.

In C, function pointers to Thumb functions will have the LSB set to 1 (so they're not the actual address of the instruction in memory), and the compiler will emit `blx` instructions.

From some quick testing in GCC and Clang, it looks like computed goto pointers *don't* have the LSB set. If the function is in Thumb mode then the compiler will emit either `orr r0, #1; bx r0` (setting LSB before interworked branch) or `mov pc, r0` (non-interworked branch). You're not allowed to computed-goto between different functions, and I don't think a single function can use a mixture of A32 and T32 instructions (except with inline assembly etc), so it's safe for the compiler to assume it's not going to switch mode.

Function pointers are not always just a pointer to the instruction

Posted Jul 23, 2025 15:08 UTC (Wed) by anton (subscriber, #25547) [Link] (1 responses)

What I actually see in code compiled with -mthumb (apparently -marm is the default for gcc-10 at least on Debian; I have seen Thumb2 code produced by default in earlier times):
orr.w   r3, r1, #1
bx      r3
Yes, if gcc produced values with a set LSB when you do &&mylabel in a Thumb-compiled function, it could then avoid the orr.w instruction in the code for goto *. However, Gforth uses the values produced by &&mylabel for determining where the code snippets (for code-copying) start and end, so if the value for the label pointed one byte later, we would need to add a workaround (e.g., force the function to compile to A32).

Function pointers are not always just a pointer to the instruction

Posted Jul 23, 2025 16:01 UTC (Wed) by anton (subscriber, #25547) [Link]

Correction: gcc-10 on Debian still compiles to T32 by default (I overlooked a -marm in Gforth).


Copyright © 2026, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds