December 4th, 2023

What is a static chain pointer in the context of calling convention ABI?

Buried deep in the System V Application Binary Interface document for the AMD64 Architecture, there is a footnote on page 24 that says, “%r10 is used for passing a function’s static chain pointer.” What is a static chain pointer?

Some languages, such as Pascal, supported nested functions such that the nested function is permitted to access variables from its parent.

function Outer(n: integer) : integer;
    var i: integer;

    procedure Inner(m: integer);
    begin
        i := i + m
    end;

(* Outer body begins here *)
begin
    i := 0;
    Inner(n);
    Outer := i
end;

The Outer function doesn’t do anything useful, but it does it in an interesting way.

It begins with a local variable declaration for i, and then defines a local procedure Inner which adds its parameter m to the Outer variable i. The Outer function then initializes i to zero, calls Inner(n) (which adds n to i), and then returns the modified value of i.

This is just the identity function, but it calculates the result with the help of an inner function.

The way this works is that the Inner function receives a hidden parameter that tells it where the Outer procedure’s local variables are.

In practice, what is passed is a pointer to the Outer procedure’s stack frame.

Now, if the containing function or precedure happens itself to be nested, then you can use the parent’s frame to access the local variables of the grandparent.

function Outer(n: integer) : integer;
    var i: integer;

    procedure Inner(m: integer);

        procedure MoreInner
        begin              
            i := i + m     
        end                
    begin
        MoreInner
    end;

(* Outer body begins here *)
begin
    i := 0;
    Inner(n);
    Outer := i
end;

In this case, the MoreInner receives a hidden pointer to Inner‘s stack frame, which lets it access the m parameter from Inner. But Inner is itself a nested procedure and therefore received a pointer to Outer‘s stack frame. Therefore, MoreInner can use that pointer to access Outer‘s local variable i.

Here’s what it looks like in a diagram:

 
MoreInner   Inner   Outer
chain chain n
    m   i

This is called a static chain because the structure of the chain is based on lexical scoping, not dynamic scoping. You can see the difference in this example:

function Outer(n: integer) : integer;
    var i: integer;

    procedure Update(j: integer);
    begin
        i := i + j
    end;

    procedure Inner(m: integer);

        procedure MoreInner;
        begin
            Update(m)
        end;

    (* Inner body begins here *)
    begin
        MoreInner
    end;

(* Outer body begins here *)
begin
    i := 0;
    Inner(n);
    Outer := i
end;

This version is even more useless than the previous one: MoreInner doesn’t update i directly, but instead asks Update (an uncle procedure) to do it.

At the point that MoreInner calls Update, it does not pass its own stack frame as the static chain pointer. Instead, it passes Outer‘s stack frame, because Update‘s parent is Outer.

The static chain does not match the dynamic call stack: The call stack says that Update‘s caller is MoreInner but the static chain says that Update‘s parent is Outer.

Update   MoreInner   Inner   Outer
frame pointer frame pointer frame pointer frame pointer
chain ➝➝➝➝➝➝➝➝➝➝➝➝➝➝➝➝ n
    chain chain i
        m    

The authors of the Application Binary Interface document assume you are familiar with how nested functions are implemented and are just noting that the calling convention for nested functions is to pass the static chain in the %r10 register.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

5 comments

Discussion is closed. Login to edit/delete existing comments.

  • Baltasar García

    When I studied computer science with Pascal as the programming language, inner functions were a Big Thing. They faded with time and especially after OO. Nowadays, apart from Pascal there is only one modern language I know that allows inner functions: Python (though, well, it was designed in the 90’s).
    Go allows them, but more as a “lambda” than a true inner function.
    It seems that the Zig people is considering it.

    • 紅樓鍮

      Well if you think about it, Pascal-style inner functions work in functionally the same way as C++ lambdas that capture by reference do: they both refer to objects that live on their (lexical) parent's stack frame, without causing them to be spilled onto the heap. And in principle, the data part of in C++ can consist of just the parent's frame pointer, regardless of how many distinct variables are captured (although I'm not aware...

      Read more
      • Dmitry

        I guess, we should define the ”vanilla Pascal” and ”first-class functions” more precisely, ’cause function pointers are a thing since at least Borland/Turbo Pascal, they are assignable, can be passed as parameters to subroutines (with all methods of passing parameters normal types have), can be returned from functions, compared, etc. That seems to be enough in general to be considered first-class citizens. Catching scopes seems to be a separate feature.

        And passing procedures as parameters is...

        Read more
      • 紅樓鍮

        I do remember Pascal supporting pointers, though my knowledge of it was limited and I wasn't aware of it supporting function pointers. However, function pointers are not quite the same as first-class functions in functional programming languages since the latter can capture free variables, and will need to be implemented either as C++-style objects with vtables or Rust-style fat pointers at low level. Both implementation strategies require the function pointer part of the function object...

        Read more
  • Dmitry

    Skipping semicolons right before end’s is not necessary, BTW, and is not quite convenient except for maybe a few cases. Pascal and Delphi do have the concept of empty statement which lets one still put semicolons there without breaking the syntax rules.