{"id":109599,"date":"2024-04-01T07:00:00","date_gmt":"2024-04-01T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=109599"},"modified":"2024-04-03T12:32:49","modified_gmt":"2024-04-03T19:32:49","slug":"20240401-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20240401-00\/?p=109599","title":{"rendered":"Subroutine calls in the ancient world, before computers had stacks or heaps"},"content":{"rendered":"<p>We take stacks and heaps for granted nowadays, but back in the very old days of computing, computers operated without a stack or a heap.<\/p>\n<p>Tell a recent college graduate this, and you may as well tell them that there was a time when you didn&#8217;t have instant access to millions of cat videos.<\/p>\n<p>It&#8217;s not too hard to imagine computing without dynamic memory allocation. You just have to use fixed-size memory buffers for everything. If you have to operate on variable-sized data, you reserved a fixed-size buffer of some capacity that is large enough to accommodate any data you would reasonably be expected to process, and if somebody asked for more, you just exited the program with a fatal error. If you were really nice, you would provide a compile-time configuration so your clients could adjust the maximum capacity to suit their datasets. And if you were really fancy, you wrote a custom allocator that operated on that fixed-size buffer so people could &#8220;allocate&#8221; and &#8220;free&#8221; memory from the buffer.<\/p>\n<p>But operating without a stack? How did you call a function if you didn&#8217;t have a stack for the return address or local variables?<\/p>\n<p>Here&#8217;s how it worked.<\/p>\n<p>First, the compiler defined a secret global variable for each inbound function parameter, plus another secret global variable for each function to hold the return address. It also defined a secret global variable for each of the function&#8217;s local variables.<\/p>\n<p>To generate a function call, the compiler assigned the parameter values to the corresponding secret global variables, assigned the return address to the function&#8217;s secret &#8220;return address variable&#8221;, and then jumped to the start of the function.<\/p>\n<p>The function read its parameters from its secret global variables, and used the pre-defined secret global variables that corresponded to its logically local variables. When the function was finished, it jumped to the address held in the function&#8217;s secret &#8220;return address variable.&#8221;<\/p>\n<p>For example, suppose you had code like this, written in a C-like language:<\/p>\n<pre>int add_two_values(int a, int b)\r\n{\r\n    int c = a + b;\r\n    return c;\r\n}\r\n\r\nvoid sample()\r\n{\r\n    int x = add_two_values(31415, 2718);\r\n}\r\n<\/pre>\n<p>This would be transformed by the compiler into something like this:<\/p>\n<pre>int a2v_a;\r\nint a2v_b;\r\nint a2v_c;\r\nvoid* a2v_retaddr;\r\n\r\nint add_two_values()\r\n{\r\n    a2v_c = a2v_a + a2v_b;\r\n\r\n    return_value_register = a2v_c;\r\n    goto a2v_retaddr;\r\n}\r\n\r\nint sample_x;\r\nvoid sample()\r\n{\r\n    a2v_a = 31415;\r\n    a2v_b = 2718;\r\n    a2v_retaddr = &amp;resume;\r\n    goto add_two_values;\r\nresume:\r\n    sample_x = return_value_register;\r\n}\r\n<\/pre>\n<p>Check it out: We did a function call and return without a stack!<\/p>\n<p>Now, you can optimize the ABI by, say, passing some of these values in registers rather than globals. For example, most processors had a special &#8220;link&#8221; register and a special instruction &#8220;branch with link&#8221; that automatically set the link register equal to the address of the instruction after the &#8220;branch with link&#8221; instruction, And maybe you optimize the calling convention to pass the first two parameters in registers, resulting in this:<\/p>\n<pre>int a2v_a;\r\nint a2v_b;\r\nint a2v_c;\r\nvoid* a2v_retaddr;\r\n\r\nint add_two_values()\r\n{\r\n    a2v_a = argument_register_1;\r\n    a2v_b = argument_register_2;\r\n    a2v_retaddr = link_register;\r\n\r\n    a2v_c = a2v_a + a2v_b;\r\n\r\n    return_value_register = a2v_c;\r\n    goto a2v_retaddr;\r\n}\r\n\r\nint sample_x;\r\nvoid sample()\r\n{\r\n    argument_register_1 = 31415;\r\n    argument_register_2 = 2718;\r\n    branch_with_link add_two_values;\r\n    sample_x = return_value_register;\r\n}\r\n<\/pre>\n<p>There was just one catch: You can&#8217;t do recursion.<\/p>\n<p>Recursion doesn&#8217;t work because a recursive call would overwrite the return-address variable with the return address of the recursive call, and when the outer call completed, it would jump to the wrong place.<\/p>\n<p>The programming languages of the day solved this problem by simply declaring it illegal: They didn&#8217;t support recursion.\u00b9<\/p>\n<p><b>Bonus chatter<\/b>: Some compilers were even sneakier and used self-modifying code: The special return-address variable was really the address field of the jump instruction at the end of the function!<\/p>\n<p>This was occasionally not so much a sneaky trick as a practical necessity: <a href=\"https:\/\/en.wikipedia.org\/wiki\/MIX\"> The processor might not support indirect jumps either<\/a>!<\/p>\n<p>After the practical value of subroutines was recognized, quite a few processors added a subroutine call instruction that worked by storing the return address at the first word of the subroutine, and beginning execution at the second word of the subroutine. To return from a subroutine, you execute an indirect jump through the subroutine start label. (As I recall, some processors stored the return address at the word <i>before<\/i> the first instruction of the subroutine.) Here&#8217;s what it looked like using a made-up assembly language:<\/p>\n<pre>add_two_values:\r\n    nop                     ; return address goes here\r\n    add   r1 = r1, r2       ; actual subroutine begins here\r\n    jmp   @add_two_values   ; indirect jump to return address\r\n\r\nsample:\r\n    mov   r1 = 31415        ; first parameter\r\n    mov   r2 = 2718         ; second parameter\r\n    bsr   add_two_values    ; call subroutine\r\n    st    sample_x = r1     ; save return value\r\n<\/pre>\n<p>When the CPU executed the <code>bsr<\/code> branch-to-subroutine instruction, it stored the return address into the first word of <code>add_two_values<\/code> (overwriting the sacrificial <code>nop<\/code>) and began execution at the following instruction, the <code>add r1 = r1, r2<\/code>.<\/p>\n<p>\u00b9 FORTRAN initially didn&#8217;t even support subroutines! Those were added in 1958. And support in FORTRAN for recursion didn&#8217;t become standard until 1991, and even then, you had to explicitly declare your subroutine as <code>RECURSIVE<\/code>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A lot of computing got done even before we had stacks and heaps.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[2],"class_list":["post-109599","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-history"],"acf":[],"blog_post_summary":"<p>A lot of computing got done even before we had stacks and heaps.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/109599","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=109599"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/109599\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=109599"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=109599"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=109599"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}