{"id":123,"date":"2026-08-27T16:59:31","date_gmt":"2026-08-27T16:59:31","guid":{"rendered":"https:\/\/blogs.igalia.com\/aboya\/?p=123"},"modified":"2026-08-27T16:59:31","modified_gmt":"2026-08-27T16:59:31","slug":"getting-perf-to-work-on-arm32-linux-part-3-the-abis","status":"publish","type":"post","link":"https:\/\/blogs.igalia.com\/aboya\/2026\/08\/27\/getting-perf-to-work-on-arm32-linux-part-3-the-abis\/","title":{"rendered":"Getting perf to work on ARM32 Linux: Part 3, the ABIs"},"content":{"rendered":"\n<p>In <a href=\"https:\/\/blogs.igalia.com\/aboya\/?p=112\">Part 2<\/a> I explained how ARM32 has two ISAs available (ARM and Thumb), how their feature sets differ in important ways and how software can and often does make use of both even in the same program.<\/p>\n\n\n\n<p>Today we&#8217;re going one level higher and look at the <em>ABI<\/em>s (<em>Application Binary Interface<\/em>). Whereas an ISA defines what bit sequences make the CPU do what operations, an ABI is a <strong>set of conventions software is meant to follow<\/strong> so that it is interoperable with other software targetting the same ABI.<\/p>\n\n\n\n<p>An ABI is adopted by compilers and operating systems. The designers of the instruction set <em>often<\/em> define some <em>base ABI<\/em> that compilers only need to extend to define a few missing bits.<\/p>\n\n\n\n<p>One of the biggest underlying reasons getting <code>perf<\/code>  to work in ARM32 is hard is that ARM has<strong> deprecated <\/strong>and gone over<strong> multiple base ABIs<\/strong> in its lifetime. Furthermore: the <strong>details<\/strong> on how <strong>frame pointers<\/strong> should work used to be completely <strong>non-standard<\/strong> and that has created <strong>fragmentation<\/strong>.<\/p>\n\n\n\n<p>As of writing, these are the standard base ABIs defined for ARM:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>~1987: <a href=\"https:\/\/developer.arm.com\/documentation\/dui0041\/c\/ARM-Procedure-Call-Standard?lang=en\">APCS<\/a> (<em>Arm Procedure Call Standard<\/em> (obsolete)). Only applies to ARM.<\/li>\n\n\n\n<li>~1997: <a href=\"https:\/\/developer.arm.com\/documentation\/dui0041\/c\/Thumb-Procedure-Call-Standard?lang=en\">TPCS<\/a> (<em>Thumb Procedure Call Standard<\/em> (obsolete)). Thumb-only counterpart of APCS.<\/li>\n\n\n\n<li>1998: <a href=\"https:\/\/developer.arm.com\/documentation\/espc0002\/latest\/\">ATPCS<\/a> (<em>ARM-THUMB Procedure Call Standard<\/em>). Unified ABI for both ARM and Thumb.<\/li>\n\n\n\n<li>2003: <a href=\"https:\/\/github.com\/ARM-software\/abi-aa\/blob\/8e73a1c3fd788fe325a3644c2129e2cfa3e3f688\/aapcs32\/aapcs32.rst\">AAPCS<\/a> (<em>Procedure Call Standard for the Arm\u00ae Architecture<\/em>). The current base ABI. It applies to both ARM and Thumb.<\/li>\n<\/ul>\n\n\n\n<p>In this post we&#8217;ll learn some typical examples of features an ABI would standardize, ultimately landing in frame pointers as one of such features.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The stack<\/h3>\n\n\n\n<p>At its most basic, a CPU does not need to have a concept of a stack. The ARM ISA exemplifies this well: the <a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-06\/Base-Instructions\/STMDB--STMFD--Store-Multiple-Decrement-Before--Full-Descending--?lang=en\">instructions used for stack manipulation<\/a> are extremely generic\u2014they can work on any register, support any kind of stack (<a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-06\/Base-Instructions\/STMDB--STMFD--Store-Multiple-Decrement-Before--Full-Descending--?lang=en\">full descending<\/a>, <a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-06\/Base-Instructions\/STMIB--STMFA--Store-Multiple-Increment-Before--Full-Ascending--\">full ascending<\/a>, <a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-06\/Base-Instructions\/STMDA--STMED--Store-Multiple-Decrement-After--Empty-Descending--\">empty descending<\/a>, <a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-06\/Base-Instructions\/STM--STMIA--STMEA--Store-Multiple--Increment-After--Empty-Ascending--?lang=en\">empty ascending<\/a>) and also work as general \u201cwrite multiple registers to memory pointed by register\u201d instructions.<\/p>\n\n\n\n<p>But of course, a stack is a very useful abstraction that any non-trivial program will need as it allows to free up registers by saving their data for later use. Different pieces of code (e.g. functions) need to <strong>agree on a definition for the stack<\/strong> and rules to use it so they don&#8217;t accidentally overwrite each other&#8217;s data.<\/p>\n\n\n\n<p>As such, ABIs define the stack; namely: what register points to the current top of the stack, whether it grows towards greater adresses (<em>ascending stack<\/em>) or toward lower adresses (<em>descending stack<\/em>), whether the \u201c<em>top of the stack<\/em>\u201d should be understood as \u201cthe last element pushed\u201d (<em>full stack<\/em>) or \u201cthe position a newly pushed element would use\u201d (<em>empty stack<\/em>), any alignment requirements, and rules for writing to the stack\u2014for example: writes outside of the stack may be considered undefined behavior even in the absence of function calls so that interrupt handlers can make use of the same stack.<\/p>\n\n\n\n<p>On ARM32, virtually all the ABIs<sup data-fn=\"39983882-7b2f-4807-bbc7-f9822cb05116\" class=\"fn\"><a href=\"#39983882-7b2f-4807-bbc7-f9822cb05116\" id=\"39983882-7b2f-4807-bbc7-f9822cb05116-link\">1<\/a><\/sup> agree on using a <strong>full-descending stack<\/strong>, whose top position is tracked by the <strong>r13 register<\/strong>, which for this reason is also give the alias <code>sp<\/code> (<em>stack pointer<\/em>). The stack is 4 byte (32 bit) aligned. The AAPCS ABI further requires 8 byte alignment at the public interface (e.g. when calling a library function). Storing data outside of the valid region of the stack (i.e. in addresses lower than <code>sp<\/code>) is undefined behavior.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Function calls<\/h3>\n\n\n\n<p>An ABI defines what a function call is under the hood; namely: how arguments are passed and how are values returned. This part of ABIs is important enough to have its own name: <em><a href=\"https:\/\/en.wikipedia.org\/wiki\/Calling_convention\">calling convention<\/a><\/em>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Function arguments<\/h4>\n\n\n\n<p>A simple calling convention could require all function arguments be to pushed into the stack. The callee could then access them by addressing the stack pointer. Notably, the <a href=\"https:\/\/bugaevc.github.io\/asmwall\/?cdecl\"><em>cdecl<\/em><\/a> and <a href=\"https:\/\/bugaevc.github.io\/asmwall\/?stdcall\"><em>stdcall<\/em><\/a> calling conventions used in MS Windows 32-bit x86 work in this manner.<\/p>\n\n\n\n<p>Modern calling conventions\u2014<strong>including ARM32 AAPCS<\/strong> and all x86-64 calling conventions\u2014<strong>use registers in addition to the stack for passing arguments<\/strong>, trading complexity in the ABI for better performance. This is especially a win for functions with few arguments. In many cases, programmers and compilers can strategically do computations directly in the same registers that will be later used as arguments, further reducing the overhead of function calls.<\/p>\n\n\n\n<p>In AAPCS, <strong>r0-r3 are filled with the first few arguments<\/strong>. Arguments with types smaller than 32 bit (e.g. <code>char<\/code>, <code>int16_t<\/code>) are extended to 32 bits before the call. When an argument has a 64-bit type, it takes two consecutive registers. For more details, see <a href=\"https:\/\/github.com\/ARM-software\/abi-aa\/blob\/2982a9f3b512a5bfdc9e3fea5d3b298f9165c36b\/aapcs32\/aapcs32.rst#65parameter-passing\">section 6.5, <em>Parameter Passing<\/em> in the AAPCS specification<\/a>. Remaining arguments are pushed into the stack.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Callee-saved and caller-saved registers<\/h4>\n\n\n\n<p>ABIs often separate user registers in two groups:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>Callee-saved registers<\/em><\/li>\n\n\n\n<li><em>Caller-saved registers<\/em>, also known as <em>scratch registers<\/em><\/li>\n<\/ul>\n\n\n\n<p>A function is allowed to write to <strong>callee-saved<\/strong> registers at any point, but it <strong>must ensure they have their original value on return<\/strong>. This is normally accomplished by pushing (<em>saving<\/em>) the previous value to the stack before writing and popping it before return, hence the name \u00ab<em>callee-saved<\/em>\u00bb.<\/p>\n\n\n\n<p>On the other hand, a function is allowed to freely write (<em><strong>scratch<\/strong><\/em>) any and all <strong>caller-saved registers<\/strong> <strong>without preserving their old value<\/strong> anywhere. This also means that, before doing any function call, you must be careful to save any important data in these registers, hence the name \u00ab<em>caller-saved<\/em>\u00bb.<\/p>\n\n\n\n<p>Having a healthy mix of callee-saved and caller-saved is good for performance, as it reduces the number of stack manipulation operations necessary in typical functions.<\/p>\n\n\n\n<p>In AAPCS, <strong>r0-r3 and r12 are caller-saved<\/strong> (i.e. scratch registers). <strong>All other general purpose registers are callee-saved<\/strong>.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Returning from a function: return pointers<\/h4>\n\n\n\n<p>The calling convention also needs to specify how returning from a function works. The <strong>caller<\/strong> must store a <em><strong>return pointer<\/strong><\/em> (an address to executable code immediately following the function call) in a well-defined place.<\/p>\n\n\n\n<p>In many calling conventions, including the common ones for x86 and x86-64, the return pointer is passed in the stack. ARM calling conventions are a bit smarter: instead they place the return pointer in a so-called <em><a href=\"https:\/\/en.wikipedia.org\/wiki\/Link_register\"><strong>link register<\/strong><\/a><\/em> (<code>lr<\/code>). This means that simple <strong>leaf<\/strong> functions (functions that do not call other functions) can operate on their arguments and return a value <strong>without ever touching the stack<\/strong>.<\/p>\n\n\n\n<p>All ARM calling conventions use <code>r14<\/code> as the link register.<\/p>\n\n\n\n<p>Of course, many functions will need to call to other functions before returning, so how does a link register handle this? Quite simply: <strong><code>lr<\/code> is a callee-saved register<\/strong>! A function that will perform function calls will typically store <code>lr<\/code> in the stack somewhere in its preamble, do as many function calls as it wants, then restore the old value of <code>lr<\/code> from the stack.<\/p>\n\n\n\n<p>The <code>l<\/code> in the <a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-06\/Base-Instructions\/BL--BLX--immediate---Branch-with-Link-and-optional-Exchange--immediate--\"><code>bl<\/code> and <code>blx<\/code><\/a> branch instructions in ARM stands for &#8220;link&#8221;. It means that the current value of <code>pc<\/code> will be loaded in <code>lr<\/code> before the branch. This is the most common way to do function calls in ARM and Thumb.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Special purpose registers<\/h3>\n\n\n\n<p>An ABI may reserve certain CPU registers for specific purposes, even if those registers would otherwise be general purpose registers according to the ISA. Compilers do to take this into account when generating code.<\/p>\n\n\n\n<p>AAPCS leaves r9 free to use for any ABI extending AAPCS (e.g. for a specific operating system).<\/p>\n\n\n\n<p>AAPCS defines r12 as the <em>Intra-Procedure-call scratch register<\/em> (<code>ip<\/code>). Functions are allowed to use it as a regular scratch register, but its real purpose is to enable writing simple <em>veneers<\/em> without needing stack manipulation.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Side-tangent: Veneers&#8230;? What is a veneer?<\/h4>\n\n\n\n<p><em>(You can safely skip this section If you just want to understand the minimum concepts necessary for frame pointers. Keep reading if you have a more general interest in ARM assembly.)<\/em><\/p>\n\n\n\n<p>Instructions in ARM have a <strong>fixed size<\/strong> of 32 bits which is as wide as pointer. A consequence of this is that calling functions in arbitrary locations of the program requires a <strong>variable number of instructions<\/strong>. Most <strong>branches<\/strong> in ARM are done with <strong>signed offsets<\/strong> with a range of \u00b132 MiB (26 bits, of which only the most significant 24 bits are actually encoded in the instruction because ARM instructions are 4-byte-aligned). <strong>Thumb<\/strong> is even scrappier: the typical Thumb <a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-06\/Base-Instructions\/BL--BLX--immediate---Branch-with-Link-and-optional-Exchange--immediate--?lang=en#iclass_t1\"><code>bl<\/code> (branch with link)<\/a> instruction only supports offsets of \u00b14MiB encoded in 22 bits, which is already more than the 16 bits per instruction of Thumb-1 and it is only possible because the <em>bl<\/em> ASM instruction gets turned into <em>two<\/em> machine Thumb-1 instructions designed for this purpose, each containing half of the offset).<\/p>\n\n\n\n<p>Compilers and linkers cope with this limitation by <strong>assuming calls will be near enough for the offset to fit<\/strong>. When the offset does not fit, they synthesize some code (the <em>veneer<\/em>, also sometimes called a <em>trampoline<\/em> or <em>range extension thunk<\/em>) in a location close enough for the offset to fit. The veneer will then load the entire address in some register\u2014likely <code>ip<\/code>\u2014 and then use a \u00abbranch to address in register\u00bb instruction (e.g. <code><a href=\"https:\/\/support.arm.com\/documentation\/ddi0597\/2026-03\/Base-Instructions\/BX--Branch-and-Exchange-\">bx<\/a> ip<\/code>).<\/p>\n\n\n\n<p>From this example the utility of having a scratch register not used for argument passing (<code>ip<\/code>) becomes apparent: if a scratch register like <code>ip<\/code> did not exist, the veneer would need to spill some callee-saved register to the stack just to build an address for the <code>bx<\/code> instruction. Furthermore, the callee couldn&#8217;t return directly into the original caller, but it would have to return back into the veneer so that the values from the stack could be restored.<\/p>\n\n\n\n<p>Also of note is that is the fact that the <code><strong>blx<\/strong><\/code> instruction (<span style=\"text-decoration: underline\">b<\/span>ranch with <span style=\"text-decoration: underline\">l<\/span>ink and e<span style=\"text-decoration: underline\">x<\/span>change ISA, provided an offset or register) <strong>did not exist until ARMv5<\/strong>. Before, there was only <strong><code>bx<\/code><\/strong> (<span style=\"text-decoration: underline\">b<\/span>ranch and e<span style=\"text-decoration: underline\">x<\/span>change ISA), which operated on a register. This resulted in a different number of instructions required for the branch depending on the ISA of the target (which may not be known until run-time in the case of shared libraries), and by extension made veneers necessary for <strong><em>interworking<\/em><\/strong> between ARM and Thumb.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Frame pointers<\/h3>\n\n\n\n<p>A <em><strong>stack frame<\/strong><\/em> is the chunk of the stack used by a specific function call, not including nested calls.<\/p>\n\n\n\n<p>A <em><strong>frame pointer<\/strong><\/em> points to a fixed location (e.g. the start or end) of the stack frame. When we say that a program uses frame pointers, we refer to that program dedicating a register\u2014the <em><strong>frame pointer register<\/strong><\/em> (usually shortened as <code>fp<\/code>)\u2014to store the frame pointer at all times.<\/p>\n\n\n\n<p>Having a frame pointer allows assembly code to refer to locations in the stack with simple offset from the frame pointer. So, for example, a certain variable saved in the stack will be at <code>fp-4<\/code> and will remain at <code>fp-4<\/code> even if code later pushes to the stack. This is more useful for humans than compilers, since it&#8217;s possible to access the same data from <code>sp<\/code> as long as you keep track of the required offset changing every time something is pushed or popped.<\/p>\n\n\n\n<p>Something much more useful occurs if code always pushes the old value of the frame pointer register and the return pointer to the stack at call boundaries: <strong>it becomes possible to inspect the stack at runtime<\/strong>: the frame pointer register points to the current frame record, from which the previous frame pointer and return pointer are accessible in a fixed offset. This process can be repeated, traversing the stack like a linked list, one stack frame at a time. Before <code>main()<\/code>, a sentinel value (typically a null pointer) should be pushed instead of a frame pointer so that the end of the linked list can be detected.<\/p>\n\n\n\n<p><strong>Note that the usage of frame pointers is an ABI matter:<\/strong> The above breaks if you call a function in a library that doesn&#8217;t use the same exact convention for frame pointers. If that function skips updating the frame pointer register, its stack frames will become invisible. Or worse: if it writes some random value to it, <strong>the entire chain of callers will no longer be recoverable<\/strong>.<\/p>\n\n\n\n<p>In x86 ABIs, the <em>Base Pointer<\/em> register (<code>bp<\/code> in 16-bit, <code>ebp<\/code> in 32-bit, <code>rbp<\/code> in 64-bits) is commonly used to store the frame pointer.<\/p>\n\n\n\n<p>Frame pointers were important for obtaining backtraces in early debuggers, but this is less necessary in modern toolchains. Nowadays compilers can generate debug information (<em><strong>debuginfo<\/strong>)<\/em> in a format like <a href=\"https:\/\/dwarfstd.org\/\">DWARF<\/a> that debuggers can use to map any value of the <code>pc<\/code> register to what function it is part of and how deep in the stack the return pointer is at that particular instruction. This is <strong>the same process they use to map local variables<\/strong> to positions in the stack. The process can be repeated until the entire stack trace is obtained.<\/p>\n\n\n\n<p>Since frame pointers became less necessary for debuggers, compiler <strong>optimizations that skip updating the frame pointer register<\/strong> or even <strong>use it as a general-purpose register<\/strong> have become very common. Omitting frame pointers as an optimization <a href=\"https:\/\/github.com\/gcc-mirror\/gcc\/commit\/c5a0877cf372229f139344b957f27174c8f67b68\">has been the default in gcc for all platforms since 2017<\/a>, after it had been enabled in most target platforms individually. <a href=\"https:\/\/github.com\/gcc-mirror\/gcc\/commit\/c17f64ccf22e11f747dec2fb47d1176c80398942#diff-5b792a13a7bf5b952680de5a119d2c12643befdc7ce73ef49bc3f14aef57286dR294\">ARM32 already had this enabled in 2010<\/a> and earlier.<\/p>\n\n\n\n<p>While debuginfo is a good alternative to frame pointers to get backtraces in a debugger, other development tools are not so lucky. Recovering <strong>call chains from frame pointers is a very lightweight affair<\/strong>: it&#8217;s a singly linked list traversal where all the nodes are within consecutive memory. On the other hand, getting a call chain through debuginfo requires having separate debuginfo sections for each object file (e.g. <code>.so<\/code> library) mapped in memory, search the wanted <code>pc<\/code> value in their tables and decode the debug tags info to find the address of the return pointer; then repeat this for every other stack frame.<\/p>\n\n\n\n<p>Some tools have tried to adapt to this new normal of not having frame-pointers by adding support for DWARF unwinding. perf is one of them. This is however still unsatisfactory on low-end hardware: <code>perf record -g --call-graph=dwarf cog about:blank<\/code> will bring a Raspberry Pi 3 to its knees, freezing the entire system until it dies on the hands of the OOM killer. High-end hardware, like desktop PCs, can usually handle using DWARF, but it still <a href=\"https:\/\/rwmj.wordpress.com\/2023\/02\/14\/frame-pointers-vs-dwarf-my-verdict\/\">results in much higher overhead<\/a>.<\/p>\n\n\n\n<p><a href=\"https:\/\/www.brendangregg.com\/blog\/2024-03-17\/the-return-of-the-frame-pointers.html\">Omitting frame pointers as an optimization has been <strong>increasingly reconsidered in recent years<\/strong><\/a>, with <a href=\"https:\/\/fedoraproject.org\/wiki\/Changes\/fno-omit-frame-pointer\">Fedora 38+<\/a>, <a href=\"https:\/\/ubuntu.com\/blog\/ubuntu-performance-engineering-with-frame-pointers-by-default\">Ubuntu 24.04+<\/a> and <a href=\"https:\/\/gitlab.archlinux.org\/archlinux\/rfcs\/-\/merge_requests\/26\">Arch Linux<\/a> modifying their build recipes to attempt to <strong>build all packages with frame <code>-fno-omit-frame-pointer<\/code><\/strong>. Modern CPUs (especially 64-bit CPUs) have enough registers that dedicating one for this purpose is not the hit to performance it once was. Some people would argue that even if there is a slight performance hit, <a href=\"https:\/\/pagure.io\/fesco\/issue\/2817#comment-824507\">the gains from users and developers being able to use profilers greatly outweight it<\/a>.<\/p>\n\n\n\n<p>With this modern understanding of what frame pointers are, we will also try to build all packages with frame pointers enabled in an ARM32 environment, so that we can also use a profiler there.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem: what are the ARM32 frame pointer ABIs, really?<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">APCS frames<\/h4>\n\n\n\n<p>The (old and deprecated) APCS spec specified an optional frame pointer ABI. When you pass the (now deprecated) <code>-mapcs-frame<\/code> flag to gcc, you tell it to use the frame pointer structures defined in APCS. Here is a descriptive diagram from page 629 of the <a href=\"https:\/\/www.4corn.co.uk\/archimedes.php\">Acorn Archimedes<\/a>&#8216; <a href=\"https:\/\/www.4corn.co.uk\/archive\/docs\/Archimedes%20Programmer's%20Reference%20Manual%20-%20Volume%202-opt.pdf\">Programmer Reference Manual<\/a> (1987), whose Appendix C is <em>\u201cARM Procedure Call Standard\u201d<\/em>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"741\" height=\"421\" src=\"https:\/\/blogs.igalia.com\/aboya\/files\/2025\/09\/APCS_frame_pointers.png\" alt=\"At the instant of an external procedure call, the value in fp is zero or it points to a data structure that gives information about the sequence of outstanding procedure calls. This structure is in the following format: fp register points to (in decreasing address order): save mask pointer, return link value, return sp value, return fp value\" class=\"wp-image-151\" srcset=\"https:\/\/blogs.igalia.com\/aboya\/files\/2025\/09\/APCS_frame_pointers.png 741w, https:\/\/blogs.igalia.com\/aboya\/files\/2025\/09\/APCS_frame_pointers-300x170.png 300w\" sizes=\"auto, (max-width: 741px) 100vw, 741px\" \/><figcaption class=\"wp-element-caption\">Note: &#8220;save mask pointer&#8221; is meant point to the instruction that pushed this structure (ostensibly at the beginning of the function). Because in ARM you can push many registers in the stack in a single instruction, that instruction effectively contains a mask of all the registers that have been saved in the stack, hence the name \u201csave mask\u201d. <\/figcaption><\/figure>\n\n\n\n<p>Here is a simple function that we will use to compare the frame pointer ABIs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>extern void show_sum(int a);\nextern void show_diff(int b);\n\nint do_operations(int a, int b) {\n    int sum = a + b;\n    show_sum(sum);\n    int diff = a - b;\n    show_diff(diff);\n    int xor = a ^ b;\n    return xor;\n}<\/code><\/pre>\n\n\n\n<p>This is the assembly generated by gcc:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ arm-linux-gcc -mcpu=cortex-a53 -marm -mapcs-frame -gdwarf-4 -fno-omit-frame-pointer -O2 -g -c add.c &amp;&amp; arm-linux-gnu-objdump -S example.o\nint do_operations(int a, int b) {\n   0:\te1a0c00d \tmov\tip, sp\n   4:\te92dd830 \tpush\t{r4, r5, fp, ip, lr, pc}\n   8:\te1a04001 \tmov\tr4, r1\n   c:\te1a05000 \tmov\tr5, r0\n  10:\te24cb004 \tsub\tfp, ip, #4\n    int sum = a + b;\n    show_sum(sum);\n  14:\te0800001 \tadd\tr0, r0, r1\n  18:\tebfffffe \tbl\t0 &lt;show_sum&gt;\n    int diff = a - b;\n    show_diff(diff);\n  1c:\te0450004 \tsub\tr0, r5, r4\n  20:\tebfffffe \tbl\t0 &lt;show_diff&gt;\n    int xor = a ^ b;\n    return xor;\n}\n  24:\te0250004 \teor\tr0, r5, r4\n  28:\te89da830 \tldm\tsp, {r4, r5, fp, sp, pc}<\/code><\/pre>\n\n\n\n<p>Here <code>ip<\/code> is used as a temporary to hold the value of <code>sp<\/code> before the <code>push<\/code>. The resulting stack matches the one in the old APCS document as we could expect:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>fp points here: | pc at time of push (i.e. savemask pointer) |\n                | saved lr                                   |\n                | ip (i.e. the saved sp before the push)     |\n                | saved fp                                   |<\/code><\/pre>\n\n\n\n<p>We can obtain the call chain by looking at the <em>fp<\/em> register and traversing the linked list of frame records: the saved <em>lr<\/em> contains the return function for that stack frame, and by extension, who called that function. We continue traversing the stack upwards until we find a NULL pointer indicating the end of the linked list.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"480\" height=\"608\" src=\"https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_apcs.png\" alt=\"\" class=\"wp-image-170\" srcset=\"https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_apcs.png 480w, https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_apcs-237x300.png 237w\" sizes=\"auto, (max-width: 480px) 100vw, 480px\" \/><figcaption class=\"wp-element-caption\">Stack diagram of APCS frame records. The <em>fp<\/em> register points to the saved <em>pc<\/em>. \u00ab<em>saved fp<\/em>\u00bb is the previous value of <em>fp<\/em>, and hence points to a previous <em>saved pc<\/em> in the stack. Since the saved registers are specified to appear always in the same number and order, the call chain can be traversed like a linked list. The linked list ends when we encounter a null <em>fp<\/em>.<\/figcaption><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">AAPCS frames, clang<\/h4>\n\n\n\n<p>It may seem weird that I just spend that much time above showing a very old ABI (APCS). Especially because if you look at the modern <a href=\"https:\/\/github.com\/ARM-software\/abi-aa\/blob\/c51addc3dc03e73a016a1e4edf25440bcac76431\/aapcs32\/aapcs32.rst#the-frame-pointer\">AAPCS spec today<\/a> you&#8217;ll see frame pointers also defined.<\/p>\n\n\n\n<p>However, if you scroll to the <em><a href=\"https:\/\/github.com\/ARM-software\/abi-aa\/blob\/c51addc3dc03e73a016a1e4edf25440bcac76431\/aapcs32\/aapcs32.rst#212change-history\">change history<\/a><\/em>, you&#8217;ll also notice it was only added in January 2020, whereas AAPCS had been around for 17 years before. <strong>This is the real source of problems:<\/strong> as far as I can tell, during these <strong>17 years<\/strong> compilers had <strong>no official guidance on any frame pointer ABI for AAPCS<\/strong>.<\/p>\n\n\n\n<p>Making things worse, <strong>GCC and clang ended up with similar but mutually incompatible frame pointer ABIs<\/strong>. The official AAPCS frame pointer ABI sided with clang&#8217;s. It looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ clang -target arm-linux-gnueabihf -g -gdwarf-4 -mcpu=cortex-a53 -c -O2 -fno-omit-frame-pointer -c example.c &amp;&amp; arm-linux-gnu-objdump -S example.o\nint do_operations(int a, int b) {\n   0:\te92d4830 \tpush\t{r4, r5, fp, lr}\n   4:\te28db008 \tadd\tfp, sp, #8\n   8:\te1a05000 \tmov\tr5, r0\n    int sum = a + b;\n   c:\te0810000 \tadd\tr0, r1, r0\n  10:\te1a04001 \tmov\tr4, r1\n    show_sum(sum);\n  14:\tebfffffe \tbl\t0 &lt;show_sum&gt;\n    int diff = a - b;\n  18:\te0450004 \tsub\tr0, r5, r4\n    show_diff(diff);\n  1c:\tebfffffe \tbl\t0 &lt;show_diff&gt;\n    int xor = a ^ b;\n  20:\te0240005 \teor\tr0, r4, r5\n    return xor;\n  24:\te8bd8830 \tpop\t{r4, r5, fp, pc}<\/code><\/pre>\n\n\n\n<p>Only two words are pushed in the stack: <em>fp<\/em> and <em>lr<\/em>. The <em>fp<\/em> register is updated to point to the the most recently saved <em>fp<\/em> in the stack.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"750\" height=\"850\" src=\"https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_aapcs.png\" alt=\"\" class=\"wp-image-171\" style=\"width:466px;height:auto\" srcset=\"https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_aapcs.png 750w, https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_aapcs-265x300.png 265w\" sizes=\"auto, (max-width: 750px) 100vw, 750px\" \/><figcaption class=\"wp-element-caption\">Stack diagram of AAPCS frame records. The <em>fp<\/em> register points directly to the previous <em>saved fp<\/em> in the stack. Adjacent to it is the <em>saved lr<\/em> containing the return address of the calling function.<\/figcaption><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">GCC frames<\/h4>\n\n\n\n<p>If you pass <code>-marm -fno-omit-frame-pointer<\/code> to gcc (as of 15.2), this is what the resulting frame pointer ABI looks like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ arm-linux-gcc -mcpu=cortex-a53 -marm -gdwarf-4 -fno-omit-frame-pointer -O2 -g -c example.c &amp;&amp; arm-linux-gnu-objdump -S example.o\nint do_operations(int a, int b) {\n   0:\te92d4830 \tpush\t{r4, r5, fp, lr}\n   4:\te1a04001 \tmov\tr4, r1\n   8:\te1a05000 \tmov\tr5, r0\n   c:\te28db00c \tadd\tfp, sp, #12\n    int sum = a + b;\n    show_sum(sum);\n  10:\te0800001 \tadd\tr0, r0, r1\n  14:\tebfffffe \tbl\t0 &lt;show_sum&gt;\n    int diff = a - b;\n    show_diff(diff);\n  18:\te0450004 \tsub\tr0, r5, r4\n  1c:\tebfffffe \tbl\t0 &lt;show_diff&gt;\n    int xor = a ^ b;\n    return xor;\n}\n  20:\te0250004 \teor\tr0, r5, r4\n  24:\te8bd8830 \tpop\t{r4, r5, fp, pc}<\/code><\/pre>\n\n\n\n<p>This is very similar to what ended up becoming the official AAPCS frame pointer ABI and to what clang does <em><strong>except that fp points to the saved lr<\/strong><\/em>, rather than to the saved <em>fp<\/em> directly. This is what it ends up looking like:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"750\" height=\"850\" src=\"https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_gcc_arm.png\" alt=\"\" class=\"wp-image-172\" style=\"width:506px;height:auto\" srcset=\"https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_gcc_arm.png 750w, https:\/\/blogs.igalia.com\/aboya\/files\/2026\/01\/stack_frame_pointers_gcc_arm-265x300.png 265w\" sizes=\"auto, (max-width: 750px) 100vw, 750px\" \/><figcaption class=\"wp-element-caption\">Stack diagram of GCC frame records when building for ARM32 with <code>-fno-omit-frame-pointer<\/code>.  The <em>fp<\/em> register points to the <em>saved lr<\/em> in the stack.<\/figcaption><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">What about Thumb?<\/h4>\n\n\n\n<p>While I&#8217;ve learned a lot about Thumb while working on this, I would be very cautious trying to make use of frame pointers in Thumb. They&#8217;re even messier and currently broken in GCC. For more details, you can keep reading this section.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">r7 as frame pointer register<\/h5>\n\n\n\n<p>Thumb in stack traces complicates things significantly. Note that <strong>r11 is a \u201chigh register\u201d<\/strong> in Thumb, which limits in how many different instructions it can be used efficiently. This is <strong>especially a problem in the original Thumb-1<\/strong>, which doesn&#8217;t have an instruction to push\/pop low registers directly and you would typically need 3 additional instructions in the prologue and 3 additional instructions in the epilogue.<\/p>\n\n\n\n<p>The above is much less of a concern with the now ubiquitous Thumb-2, which has 32-bit instructions for push\/pop and arithmetic on high registers. However, as a consequence of this historical problem, you will see that in a default Linux setup <strong>both gcc and clang use r7 as frame pointer register in Thumb code<\/strong>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ clang -target arm-linux-gnueabihf -mcpu=cortex-a53 -mthumb -gdwarf-4 -fno-omit-frame-pointer -O2 -g -c example.c &amp;&amp; arm-linux-gnu-objdump -S example.o\nint do_operations(int a, int b) {\n   0:\tb5b0      \tpush\t{r4, r5, r7, lr}\n   2:\taf02      \tadd\tr7, sp, #8\n   4:\t4605      \tmov\tr5, r0\n    int sum = a + b;\n   6:\t4408      \tadd\tr0, r1\n   8:\t460c      \tmov\tr4, r1\n    show_sum(sum);\n   a:\tf7ff fffe \tbl\t0 &lt;show_sum&gt;\n    int diff = a - b;\n   e:\t1b28      \tsubs\tr0, r5, r4\n    show_diff(diff);\n  10:\tf7ff fffe \tbl\t0 &lt;show_diff&gt;\n    int xor = a ^ b;\n  14:\tea84 0005 \teor.w\tr0, r4, r5\n    return xor;\n  18:\tbdb0      \tpop\t{r4, r5, r7, pc}<\/code><\/pre>\n\n\n\n<p>The layout clang uses is the <strong>same as the official AAPCS ABI, but using r7 instead of the r11<\/strong>. Assuming there is no mixing of ARM and Thumb \u2014 the call chain can be recovered with the same traversal algorithm.<\/p>\n\n\n\n<p><strong>On GCC, however, Thumb frame pointers are unviable for call chain recovery<\/strong>, as the GCC <a href=\"https:\/\/bugs.llvm.org\/show_bug.cgi?id=18505#c5\">sets r7 to the most recent position in the stack<\/a>, which makes it impossible to know the position of the saved lr and the previous node pointer within the stack without additional information.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">Using r11 for both ARM and Thumb<\/h5>\n\n\n\n<p>Using different frame pointer registers for different modes is problematic, as code in one mode (typically ARM) will <strong>frequently clobber the register used by the other mode<\/strong> (typically Thumb). Both r7 and r11 are caller-saved registers, so the information is there in the stack, but the stack and register values is not enough to locate them.<\/p>\n\n\n\n<p>A simpler approach is to use r11 for both modes, which is much less awkward in Thumb-2. This is what the official AAPCS spec specifies. Recent versions of <strong>clang<\/strong> can be told to do this with <code><strong>-mframe-chain=aapcs<\/strong><\/code> (or alternatively, <code>-mframe-chain=aapcs+leaf<\/code> if frame pointers on leaf functions are also desired).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ clang -target arm-linux-gnueabihf -mcpu=cortex-a53 -mthumb -gdwarf-4 -fno-omit-frame-pointer -mframe-chain=aapcs -O2 -g -c example.c &amp;&amp; arm-linux-gnu-objdump -S example.o\nint do_operations(int a, int b) {\n   0:\te92d 4830 \tstmdb\tsp!, {r4, r5, fp, lr}\n   4:\tf10d 0b08 \tadd.w\tfp, sp, #8\n   8:\t4605      \tmov\tr5, r0\n    int sum = a + b;\n   a:\t4408      \tadd\tr0, r1\n   c:\t460c      \tmov\tr4, r1\n    show_sum(sum);\n   e:\tf7ff fffe \tbl\t0 &lt;show_sum&gt;\n    int diff = a - b;\n  12:\t1b28      \tsubs\tr0, r5, r4\n    show_diff(diff);\n  14:\tf7ff fffe \tbl\t0 &lt;show_diff&gt;\n    int xor = a ^ b;\n  18:\tea84 0005 \teor.w\tr0, r4, r5\n    return xor;\n  1c:\te8bd 8830 \tldmia.w\tsp!, {r4, r5, fp, pc}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Frame pointer sadness<\/h3>\n\n\n\n<p>There are problems with frame pointers in ARM other than the incompatible ABIs. In fact, I kept finding more and more sadness the more I worked on this post.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">GCC frames can&#8217;t be unwinded in leaf functions<\/h4>\n\n\n\n<p>While <code>-fno-omit-frame-pointer<\/code> will make GCC push <em>fp<\/em> to the stack, it doesn&#8217;t disable the \u201clink register save elimination\u201d optimization. This messes up with leaf functions. Consider the following trivial function:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void simple_nop(void) {\n    __asm__(\"nop\");\n}<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>$ arm-linux-gcc -mcpu=cortex-a53 -marm -gdwarf-4 -fno-omit-frame-pointer -O2 -g -c simple_nop.c -S &amp;&amp; cat simple_nop.s\nsimple_nop:\n\t@ args = 0, pretend = 0, frame = 0\n\t@ frame_needed = 1, uses_anonymous_args = 0\n\t@ link register save eliminated.\n\tstr\tfp, &#091;sp, #-4]!\n\tadd\tfp, sp, #0\n\t.syntax divided\n@ 31 \"example.c\" 1\n\tnop\n@ 0 \"\" 2\n\t.arm\n\t.syntax unified\n\tadd\tsp, fp, #0\n\t@ sp needed\n\tldr\tfp, &#091;sp], #4\n\tbx\tlr\n\t.size\tsimple_nop, .-simple_nop<\/code><\/pre>\n\n\n\n<p>Notice that, unlike in the previously shown GCC frames, here the <em>fp<\/em> register is made to point to the saved <em>fp<\/em> in the stack, rather than the saved <em>lr<\/em>. This means that an unwinder will <strong>(1)<\/strong> <strong>wrongly read<\/strong> a <em>next node<\/em> pointer instead of the function return address that it would expect, <strong>(2)<\/strong> will interpret <strong>whatever random variable<\/strong> was pushed in the stack immediately before as the <em>next node<\/em> in the call chain.<\/p>\n\n\n\n<p>I&#8217;m surprised I only caught this long after having written an unwinder for GCC frames and having seen it produce sensible flamecharts, but I&#8217;m pretty sure this is not just a synthetic problem, since I can see the same problematic assembly by objdump&#8217;ing GStreamer libraries.<\/p>\n\n\n\n<p>The deprecated APCS frames (<code>-mapcs-frame<\/code>) are not affected by this bug.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Getting the top frame is racy during the prologue<\/h4>\n\n\n\n<p>At the top of a stack trace you expect to see the currently running function, at the currently running instruction. This is normally obtained by reading the value of the <em>pc<\/em> register. Then, by looking at the <em>fp<\/em> register and inspecting the topmost frame record we can see who called this function and traverse the chain.<\/p>\n\n\n\n<p>However, this also means there is a small window of time from the moment a call is done with <code>bl<\/code> and until the <em>fp<\/em> register is updated to point to the newly created frame record in which the <em>pc<\/em> register points to the callee and the caller is recorded in the <em>lr<\/em> register, but hasn&#8217;t been put in a frame record yet.<\/p>\n\n\n\n<p>This results on the caller function seemingly disappearing from the stack trace for a brief moment, as seen here in a instruction-per-instruction simulation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>* 1000(main+0)\n* 1004(main+4)\n* 1008(main+8)\n* 1012(main+12)\n* 2000(leaf1+0)\n* 2004(leaf1+4)\n* 1012(main+12) &lt;- 2008(leaf1+8)\n* 1012(main+12) &lt;- 2012(leaf1+12)\n* 1012(main+12) &lt;- 2016(leaf1+16)<\/code><\/pre>\n\n\n\n<p>I can&#8217;t think of an alternative call chain traversal algorithm that solves this problem using only the stack and registers. While it seems tempting to also make use of the <em>lr<\/em> register directly to reconstruct the missing frame, that causes other problems later when the callee returns. <a href=\"https:\/\/developers.redhat.com\/articles\/2024\/10\/30\/limitations-frame-pointer-unwinding#function_prologues_and_epilogues\">I&#8217;m also not the first person to point out this problem.<\/a><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Conclusions<\/h3>\n\n\n\n<p>Frame pointers in ARM32 are messy \u2014 especially compared to ARM64 or x86_64. A standard ABI for them has only been specified few years ago, with support for it is pretty spotty and divided into incompatible alternatives.<\/p>\n\n\n\n<p>The status of frame pointers for ARM32 in GCC is much worse than I thought when I started working on this: my impression now is that the ostensibly deprecated <code>-mapcs-frame<\/code> is the only frame pointer ABI GCC handles in a reliable way, and I wouldn&#8217;t be too surprised if it turned out that any usefulness of the non-APCS frame pointers in ARM32 with GCC is coincidental rather than intentional. This is certainly not helped by the long lasting specification void in this area.<\/p>\n\n\n\n<p>The default frame pointer ABI of GCC is only reliable during non-leaf functions and as long as Thumb is not used. With that big caveat, they&#8217;re still potentially useful. Furthermore, the GCC-specific deficiencies I&#8217;ve explained here could be fixed, especially now that there is some official ABI guidance.<\/p>\n\n\n\n<p>Clang has positively surprised me during my investigation. I started looking at it only for comparison, but was happy to see support for the new AAPCS frame pointer ABI in Thumb as well, as well as a general good handling of edge cases.<\/p>\n\n\n\n<p>On top of that, seeing the limitations of frame pointers has also made me a bit curious about the alternative approaches for improved accuracy. Most of those require lookup tables and therefore are generally going to be slower, but how much they can be optimized is still an area of ongoing research.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Additional resources<\/h3>\n\n\n\n<p>Analyzing code that uses the stack by hand is error prone, so eventually I <a href=\"https:\/\/gist.github.com\/ntrrgc\/250fbc28607d8e0057115165c13313ac\">wrote a simulator<\/a> for testing frame pointer ABIs and the algorithms to traverse them.<\/p>\n\n\n\n<p>If you want to look at the compiler sources, you&#8217;ll be mostly intersted in:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>clang: <code>llvm\/lib\/Target\/ARM\/ARMFrameLowering.cpp<\/code>. Contains the code for generating prologues and epilogues. Search <code>FramePtr<\/code> to find the relevant code.<\/li>\n\n\n\n<li>gcc: <code>gcc\/config\/arm\/arm.c<\/code>. Look for <code>HARD_FRAME_POINTER_REGNUM<\/code>. The prologue contents are emitted in <code>arm_expand_prologue()<\/code>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n\n\n<ol class=\"wp-block-footnotes\"><li id=\"39983882-7b2f-4807-bbc7-f9822cb05116\">The earliest variant of APCS\u2014referred to as APCS-A\u2014mapped sp to r12 instead of r13. fp was also mapped to r10 instead of r11. This is explained in page 1762 of the <a href=\"https:\/\/www.4corn.co.uk\/archive\/docs\/RISC%20OS%20Programmer's%20Reference%20Manual%20-%20Volume%204-opt.pdf\">RISC OS Programmer Reference Manual (1989)<\/a>, which describes what would <a href=\"https:\/\/www.cl.cam.ac.uk\/~fms27\/teaching\/2001-02\/arm-project\/02-sort\/apcs.txt\">later<\/a> be called APCS-2.<br>I&#8217;m also leaving aside many complexities of APCS variants, most notably <em>chunked stacks<\/em>, where the program stack wouldn&#8217;t be in contiguous memory, but instead in a linked list of chunks, useful for multi-threading in processors without a <a href=\"https:\/\/en.wikipedia.org\/wiki\/Memory_management_unit\">Memory Management Unit<\/a>. <a href=\"#39983882-7b2f-4807-bbc7-f9822cb05116-link\" aria-label=\"Jump to footnote reference 1\">\u21a9\ufe0e<\/a><\/li><\/ol>","protected":false},"excerpt":{"rendered":"<p>In Part 2 I explained how ARM32 has two ISAs available (ARM and Thumb), how their feature sets differ in important ways and how software can and often does make use of both even in the same program. Today we&#8217;re going one level higher and look at the ABIs (Application Binary Interface). Whereas an ISA &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/blogs.igalia.com\/aboya\/2026\/08\/27\/getting-perf-to-work-on-arm32-linux-part-3-the-abis\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Getting perf to work on ARM32 Linux: Part 3, the ABIs&#8221;<\/span><\/a><\/p>\n","protected":false},"author":57,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"[{\"id\":\"39983882-7b2f-4807-bbc7-f9822cb05116\",\"content\":\"The earliest variant of APCS\\u2014referred to as APCS-A\\u2014mapped sp to r12 instead of r13. fp was also mapped to r10 instead of r11. This is explained in page 1762 of the <a href=\\\"https:\\\/\\\/www.4corn.co.uk\\\/archive\\\/docs\\\/RISC%20OS%20Programmer's%20Reference%20Manual%20-%20Volume%204-opt.pdf\\\">RISC OS Programmer Reference Manual (1989)<\\\/a>, which describes what would <a href=\\\"https:\\\/\\\/www.cl.cam.ac.uk\\\/~fms27\\\/teaching\\\/2001-02\\\/arm-project\\\/02-sort\\\/apcs.txt\\\">later<\\\/a> be called APCS-2.<br>I'm also leaving aside many complexities of APCS variants, most notably <em>chunked stacks<\\\/em>, where the program stack wouldn't be in contiguous memory, but instead in a linked list of chunks, useful for multi-threading in processors without a <a href=\\\"https:\\\/\\\/en.wikipedia.org\\\/wiki\\\/Memory_management_unit\\\">Memory Management Unit<\\\/a>.\"}]"},"categories":[1],"tags":[],"class_list":["post-123","post","type-post","status-publish","format-standard","hentry","category-uncategorized","entry"],"_links":{"self":[{"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/posts\/123","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/users\/57"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/comments?post=123"}],"version-history":[{"count":44,"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/posts\/123\/revisions"}],"predecessor-version":[{"id":182,"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/posts\/123\/revisions\/182"}],"wp:attachment":[{"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/media?parent=123"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/categories?post=123"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.igalia.com\/aboya\/wp-json\/wp\/v2\/tags?post=123"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}