mirror of
https://github.com/pkivolowitz/asm_book.git
synced 2026-06-23 07:28:04 +08:00
added a good deal including some history
This commit is contained in:
parent
d2cbef641c
commit
6c47f3b08a
2 changed files with 232 additions and 91 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
# Section 1 / Calling and Returning From Functions
|
# Section 1 / Calling and Returning From Functions
|
||||||
|
|
||||||
Calling functions, passing parameters to them and receiving back return
|
Calling functions, passing parameters to them and receiving back return
|
||||||
values is basic to using `C` and and `C++`. Calling methods (which are
|
values is basic to using `C` and and `C++`.
|
||||||
functions connected to objects) is similar but with enough differences
|
|
||||||
to warrant its own discussion to be provided later in the chapter on
|
Calling *methods* (which are functions connected to objects) is similar
|
||||||
[structs](../structs/using.md).
|
but with enough differences to warrant its own discussion to be provided
|
||||||
|
later in the chapter on [structs](../structs/using.md).
|
||||||
|
|
||||||
Be sure to read [this](./README2.md) for information about
|
Be sure to read [this](./README2.md) for information about
|
||||||
passing parameters to functions.
|
passing parameters to functions.
|
||||||
|
|
@ -169,7 +170,7 @@ First, let's take a trip back in time to the early days of C.
|
||||||
[Stephen Bourne](https://en.wikipedia.org/wiki/Stephen_R._Bourne) was
|
[Stephen Bourne](https://en.wikipedia.org/wiki/Stephen_R._Bourne) was
|
||||||
writing `sh`, the first shell for Unix. He noticed that every function
|
writing `sh`, the first shell for Unix. He noticed that every function
|
||||||
had to return a value - even functions that had no reason to return
|
had to return a value - even functions that had no reason to return
|
||||||
a value.
|
a value.
|
||||||
|
|
||||||
In these early days, `void` functions did not yet exist.
|
In these early days, `void` functions did not yet exist.
|
||||||
|
|
||||||
|
|
@ -232,6 +233,12 @@ ReturnsADouble: // 13
|
||||||
Note, the use of the floating point move instruction as well as the
|
Note, the use of the floating point move instruction as well as the
|
||||||
single precision and double precision registers.
|
single precision and double precision registers.
|
||||||
|
|
||||||
|
## Inline functions
|
||||||
|
|
||||||
|
Functions that are declared as *inline* don't actually make function
|
||||||
|
calls. Instead, the code from the function is type checked and inserted
|
||||||
|
directly where the "call" is made after adjusting for parameter names.
|
||||||
|
|
||||||
## Repeating the TL;DR
|
## Repeating the TL;DR
|
||||||
|
|
||||||
If your functions call *any* other functions, `x30` must be backed
|
If your functions call *any* other functions, `x30` must be backed
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,30 @@
|
||||||
# Section 1 / Passing Parameters To Functions
|
# Section 1 / Passing Parameters To Functions
|
||||||
|
|
||||||
Up to 8 parameters can be passed directly via registers. Each parameter can be up to the size of an address, long or double (8 bytes). If you need to pass more than 8 parameters or you need to pass parameters which are larger than 8 bytes or are `structs`, you would use a different technique described later.
|
How parameters are passed to functions can be different from OS to OS.
|
||||||
|
This chapter is written to the standard implemented for Linux. It
|
||||||
|
differs from the **calling convention** used on, for example, the Mac in
|
||||||
|
that parameters are principally passed via the scratch registers.
|
||||||
|
|
||||||
For the purposes of the present discussion, we assume all parameters are `long int` and are therefore stored in `x` registers.
|
Up to 8 parameters can be passed directly via registers. Each parameter
|
||||||
|
can be up to the size of an address, long or double (8 bytes). If you
|
||||||
|
need to pass more than 8 parameters or you need to pass parameters which
|
||||||
|
are larger than 8 bytes or are `structs`, you would use a different
|
||||||
|
technique described later.
|
||||||
|
|
||||||
Up to 8 parameters are passed in the scratch registers (of which there are 8). These are `x0` through `x7`. *Scratch* means the value of the register can be changed at will without any need to backup or restore their values.
|
Remember that even large data structures that are passed by reference
|
||||||
|
are, in fact, passed via their base address (as a pointer).
|
||||||
|
|
||||||
**This means that you cannot count on the contents of the scratch registers maintaining their value if your function makes any function calls.**
|
For the purposes of the present discussion, we assume all parameters are
|
||||||
|
`long int` and are therefore stored in `x` registers.
|
||||||
|
|
||||||
|
Up to 8 parameters are passed in the scratch registers (of which there
|
||||||
|
are a matching 8). These are `x0` through `x7`. *Scratch* means the
|
||||||
|
value of the register can be changed at will without any need to backup
|
||||||
|
or restore their values.
|
||||||
|
|
||||||
|
**This means that you cannot count on the contents of the scratch
|
||||||
|
registers maintaining their value if your function makes any function
|
||||||
|
calls.**
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
|
@ -24,83 +42,34 @@ func: add x0, x0, x1 // 1
|
||||||
ret // 2
|
ret // 2
|
||||||
```
|
```
|
||||||
|
|
||||||
The first parameter (`p1`) goes in the first scratch register (`x0`). It's an `x` because the parameter's type is `long int`. It is the `0` register, because that is the first scratch register.
|
The value of the first parameter (`p1`) is copied into the first scratch
|
||||||
|
register (`x0`). It's an `x` because the parameter's type is `long int`.
|
||||||
|
It is the `0` register, because that is the first scratch register.
|
||||||
|
|
||||||
The second parameter (`p2`) goes in the second scratch register (the `1` register) because it is the second argument, and so on.
|
The value of the second parameter (`p2`) is copied into the second
|
||||||
|
scratch register (the `1` register) because it is the second argument,
|
||||||
|
and so on.
|
||||||
|
|
||||||
`Line 1` of the assembly language provides the label `func` to which a `bl` can be made.
|
`Line 1` of the assembly language provides the label `func` to which a
|
||||||
|
`bl` can be made.
|
||||||
|
|
||||||
`Line 1` also provides the full body of the function - the third argument to `add` is added to the second and the result is put in the first. Thus it is: `x0 = x0 + x1`.
|
`Line 1` also provides the full body of the function - the third
|
||||||
|
argument to `add` is added to the second and the result is put in the
|
||||||
|
first. Thus it is: `x0 = x0 + x1`.
|
||||||
|
|
||||||
Just as scratch registers are used for passing (up to 8) parameters, the `0` register is used for function returns.
|
Just as scratch registers are used for passing (up to 8) parameters, the
|
||||||
In the case of the current code, the result of the addition is already sitting in `x0` so all we do is `ret` on `Line 2`.
|
`0` register is used for function returns. In the case of the current
|
||||||
|
code, the result of the addition is already sitting in `x0` so all we do
|
||||||
|
is `ret` on `Line 2`.
|
||||||
|
|
||||||
## Passing Pointers
|
**This is an advanced topic:** If you are the author of both the caller
|
||||||
|
and the callee and both are in assembly language, you can play loosey
|
||||||
|
goosey with how you return values. Specifically, you can return more
|
||||||
|
than one value. **But** if you do so, you give up the possibility of
|
||||||
|
calling these functions from C or C++. Maybe you should forget you read
|
||||||
|
this paragraph?
|
||||||
|
|
||||||
A pointer is an address of something. The word *pointer* is scary. The words *address of* are not as scary. They mean **exactly** the same thing.
|
## `const`
|
||||||
|
|
||||||
Here is a function which *also* adds two parameters together but this time using pointers to `long int` rather than the values themselves.
|
|
||||||
|
|
||||||
```c
|
|
||||||
void func(long * p1, long * p2) // 1
|
|
||||||
{ // 2
|
|
||||||
*p1 = *p1 + *p2; // 3
|
|
||||||
} // 4
|
|
||||||
```
|
|
||||||
|
|
||||||
`Line 1` passes the *address of* `p1` and `p2` as parameters. That is, the addresses of `p1` and `p2` are passed in registers `x0` and `x1` rather than their contents. The contents of the underlying
|
|
||||||
longs still reside in memory. That is:
|
|
||||||
|
|
||||||
* The address of `p1` arrives in `x0`.
|
|
||||||
* The value of `p1` is found in memory at the address specified by the parameter.
|
|
||||||
|
|
||||||
`Line 3` *dereferences* the addresses to fetch their underlying values.
|
|
||||||
The values are added together and the result overwrites the value pointed to by `p1`.
|
|
||||||
|
|
||||||
Here it is in assembly language:
|
|
||||||
|
|
||||||
```asm
|
|
||||||
func: ldr x2, [x0] // 1
|
|
||||||
ldr x3, [x1] // 2
|
|
||||||
add x2, x2, x3 // 3
|
|
||||||
str x2, [x0] // 4
|
|
||||||
ret // 5
|
|
||||||
```
|
|
||||||
|
|
||||||
The `add` instruction cannot operate on values in memory. With little exception, all the *action* takes place in registers, not memory. Therefore, the underlying values pointed to by the parameters must be fetched from memory.
|
|
||||||
|
|
||||||
`Line 1` provides the label to which a use of `bl` can branch with link register.
|
|
||||||
|
|
||||||
Remember that up to the first 8 parameters are passed in the 8 scratch registers. Thus, the address of `p1` and the address of `p2` are stored in `x0` and `x1` respectively. `0` and `1` because these are the first two parameters. The
|
|
||||||
`x` form of the `0` and `1` registers are used because the parameters' type are addresses.
|
|
||||||
|
|
||||||
* Addresses (pointers) to any type are 64 bits wide and therefore must use `x` registers.
|
|
||||||
* `long` and `unsigned long` integers are 64 bits wide and ...
|
|
||||||
* `double` floats are 64 bits wide
|
|
||||||
|
|
||||||
`Line 1` also dereferences the address held in `x0` going out to memory and loading (`ldr`) the value found there into `x2`, another scratch register. It's scratch so it doesn't need backing up and restoring.
|
|
||||||
|
|
||||||
`Line 2` does the same for `p2`, putting its value in `x3`.
|
|
||||||
|
|
||||||
Why didn't we reuse `x0` and `x1` as in:
|
|
||||||
|
|
||||||
```asm
|
|
||||||
ldr x0, [x0]
|
|
||||||
ldr x1, [x1]
|
|
||||||
```
|
|
||||||
|
|
||||||
Doing so would be legal but would end in tears. Doing so would blow away the address of `p1` (and `p2` too
|
|
||||||
but this doesn't matter). Destroying the address of `p1` would prevent us from copying the result of the
|
|
||||||
addition back into memory since the address to which we would want to store the result of the addition
|
|
||||||
would be gone. Can't have that!
|
|
||||||
|
|
||||||
So, as the smart *human*, we decided to use `x2` and `x3` because, well, they're scratch.
|
|
||||||
|
|
||||||
`Line 3` performs the addition.
|
|
||||||
|
|
||||||
`Line 4` stored the value in `x2` at the address in memory still sitting in `x0`.
|
|
||||||
|
|
||||||
### `const`
|
|
||||||
|
|
||||||
Suppose we had:
|
Suppose we had:
|
||||||
|
|
||||||
|
|
@ -115,7 +84,105 @@ how would the assembly language change?
|
||||||
|
|
||||||
Answer: no change at all!
|
Answer: no change at all!
|
||||||
|
|
||||||
`const` is an instruction to the compiler ordering it to prohibit changing the values of `p1` and `p2`. We're smart humans and realize that our assembly language makes no attempt to change `p1` and `p2` so no changes are warranted.
|
`const` is an instruction to the compiler ordering it to prohibit
|
||||||
|
changing the values of `p1` and `p2`. We're smart humans and realize
|
||||||
|
that our assembly language makes no attempt to change `p1` and `p2` so
|
||||||
|
no changes are warranted.
|
||||||
|
|
||||||
|
## Passing Pointers
|
||||||
|
|
||||||
|
A pointer is an address of something. The word *pointer* is scary. The
|
||||||
|
words *address of* are not as scary. They mean **exactly** the same
|
||||||
|
thing.
|
||||||
|
|
||||||
|
Here is a function which *also* adds two parameters together but this
|
||||||
|
time using pointers to `long int` rather than the values themselves.
|
||||||
|
|
||||||
|
```c
|
||||||
|
void func(long * p1, long * p2) // 1
|
||||||
|
{ // 2
|
||||||
|
*p1 = *p1 + *p2; // 3
|
||||||
|
} // 4
|
||||||
|
```
|
||||||
|
|
||||||
|
`Line 1` passes the *address of* `p1` and `p2` as parameters. That is,
|
||||||
|
the addresses of `p1` and `p2` are passed in registers `x0` and `x1`
|
||||||
|
rather than their contents. The contents of the underlying longs still
|
||||||
|
reside in memory. That is:
|
||||||
|
|
||||||
|
* The address of `p1` arrives in `x0`. The value of `p1` still
|
||||||
|
resides in memory.
|
||||||
|
|
||||||
|
* The value of `p1` is found in memory at the address specified by the
|
||||||
|
parameter.
|
||||||
|
|
||||||
|
`Line 3` *dereferences* the addresses to fetch their underlying values.
|
||||||
|
The values are added together and the result overwrites the value
|
||||||
|
pointed to by `p1`.
|
||||||
|
|
||||||
|
Here it is in assembly language:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
func: ldr x2, [x0] // 1
|
||||||
|
ldr x3, [x1] // 2
|
||||||
|
add x2, x2, x3 // 3
|
||||||
|
str x2, [x0] // 4
|
||||||
|
ret // 5
|
||||||
|
```
|
||||||
|
|
||||||
|
The `add` instruction cannot operate on values in memory.
|
||||||
|
|
||||||
|
With little exception, all the *action* takes place in registers, not
|
||||||
|
memory. Therefore, the underlying values pointed to by the parameters
|
||||||
|
must be fetched from memory.
|
||||||
|
|
||||||
|
`Line 1` provides the label to which a use of `bl` can branch with link
|
||||||
|
register.
|
||||||
|
|
||||||
|
Remember that up to the first 8 parameters are passed in the 8 scratch
|
||||||
|
registers. Thus, the address of `p1` and the address of `p2` are stored
|
||||||
|
in `x0` and `x1` respectively. `0` and `1` because these are the first
|
||||||
|
two parameters. The `x` form of the `0` and `1` registers are used
|
||||||
|
because the parameters' type are addresses.
|
||||||
|
|
||||||
|
* Addresses (pointers) to any type are 64 bits wide and therefore must
|
||||||
|
use `x` registers.
|
||||||
|
|
||||||
|
* `long` and `unsigned long` integers are 64 bits wide and ...
|
||||||
|
|
||||||
|
* `double` floats are 64 bits wide
|
||||||
|
|
||||||
|
`Line 1` also dereferences the address held in `x0` going out to memory
|
||||||
|
and loading (`ldr`) the value found there into `x2`, another scratch
|
||||||
|
register. It's scratch so it doesn't need backing up and restoring.
|
||||||
|
|
||||||
|
`Line 2` does the same for `p2`, putting its value in `x3`.
|
||||||
|
|
||||||
|
To say this again but differently, the syntax `[` then an `x` register
|
||||||
|
followed by `]` means use the `x` register as an address in RAM. Go
|
||||||
|
to that address and fetch its value. This is a *dereference*.
|
||||||
|
|
||||||
|
Why didn't we reuse `x0` and `x1` as in:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
ldr x0, [x0]
|
||||||
|
ldr x1, [x1]
|
||||||
|
```
|
||||||
|
|
||||||
|
Doing so would be legal but would end in tears.
|
||||||
|
|
||||||
|
Doing so would blow away the address of `p1` (and `p2` too). Destroying
|
||||||
|
the address of `p1` would prevent us from copying the result of the
|
||||||
|
addition back into memory since the address to which we would want to
|
||||||
|
store the result of the addition would be gone. Can't have that!
|
||||||
|
|
||||||
|
So, as the smart *human*, we decided to use `x2` and `x3` because, well,
|
||||||
|
they're scratch.
|
||||||
|
|
||||||
|
`Line 3` performs the addition.
|
||||||
|
|
||||||
|
`Line 4` stored the value in `x2` at the address in memory still sitting
|
||||||
|
in `x0`.
|
||||||
|
|
||||||
### Passing by Reference
|
### Passing by Reference
|
||||||
|
|
||||||
|
|
@ -130,22 +197,56 @@ long func(long & p1, long & p2) // 1
|
||||||
|
|
||||||
how would the assembly language change?
|
how would the assembly language change?
|
||||||
|
|
||||||
Answer: no change at all!
|
Answer: just a little:
|
||||||
|
|
||||||
Passing by reference is also an instruction to the compiler to treat pointers a little differently - the differences don't show up here so there is no change needed to the assembly language we wrote to handle passing pointers.
|
```asm
|
||||||
|
func: ldr x2, [x0] // 1
|
||||||
|
ldr x3, [x1] // 2
|
||||||
|
add x2, x2, x3 // 3
|
||||||
|
mov x0, x2 // 4
|
||||||
|
ret // 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Passing by reference is also an instruction to the compiler to treat
|
||||||
|
pointers a little differently - the differences don't show up here so
|
||||||
|
there the only change to our pointer passing version is how we return
|
||||||
|
the answer.
|
||||||
|
|
||||||
|
But wait...
|
||||||
|
|
||||||
|
There is a small optimization we can make here:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
func: ldr x0, [x0] // 1
|
||||||
|
ldr x1, [x1] // 2
|
||||||
|
add x0, x0, x1 // 3
|
||||||
|
ret // 4
|
||||||
|
```
|
||||||
|
|
||||||
|
This time we're not storing anything back to `p1` or `p2` so we can
|
||||||
|
reuse `x0` and `x1` since the addresses they contained aren't needed
|
||||||
|
again. Smart human!
|
||||||
|
|
||||||
## What If We Need More Than Eight Parameters?
|
## What If We Need More Than Eight Parameters?
|
||||||
|
|
||||||
First, do you **really** need to pass more than 8 parameters? **REALLY?**
|
First, do you **really** need to pass more than 8 parameters?
|
||||||
|
**REALLY?**
|
||||||
|
|
||||||
If for some reason you do, you can pass the first 8 in registers are described above. Beginning with the ninth parameters, these would be passed on the stack.
|
If for some reason you do, you can pass the first 8 in registers are
|
||||||
|
described above. Beginning with the ninth parameter, these would be
|
||||||
|
passed on the stack.
|
||||||
|
|
||||||
**REMEMBER THAT ANY ADJUSTMENT TO THE STACK MUST BE DONE IN MULTIPLES OF 16!**
|
**REMEMBER THAT ANY ADJUSTMENT TO THE STACK MUST BE DONE IN MULTIPLES OF
|
||||||
|
16!**
|
||||||
|
|
||||||
* If you need just 1 byte of stack, the stack pointer must be changed by 16.
|
* If you need just 1 byte of stack, the stack pointer must be changed by
|
||||||
* If you need 17 bytes of stack, the stack pointer must be changed by 32 and so on.
|
16.
|
||||||
|
|
||||||
Here is a sample function that requires 9 parameters (for who knows what reason):
|
* If you need 17 bytes of stack, the stack pointer must be changed by 32
|
||||||
|
and so on.
|
||||||
|
|
||||||
|
Here is a sample function that requires 9 parameters (for who knows what
|
||||||
|
reason):
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
@ -204,7 +305,10 @@ fmt: .asciz "This example hurts: %ld %ld\n" // 30
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice how `main()` puts the first 8 parameters into the scratch registers `x0` through `x7` using `Lines 17` to `24`. But first, it put the ninth parameter on the stack. It did the stack parameter first so that the stack pointer could be manipulated in a scratch register.
|
Notice how `main()` puts the first 8 parameters into the scratch
|
||||||
|
registers `x0` through `x7` using `Lines 17` to `24`. But first, it put
|
||||||
|
the ninth parameter onto the stack. It did the stack parameter first so
|
||||||
|
that the stack pointer could be manipulated in a scratch register.
|
||||||
|
|
||||||
After executing `Line 14`, the stack will have:
|
After executing `Line 14`, the stack will have:
|
||||||
|
|
||||||
|
|
@ -233,4 +337,34 @@ sp + 32 return address for main
|
||||||
sp + 40 zero
|
sp + 40 zero
|
||||||
```
|
```
|
||||||
|
|
||||||
This means that `Line 8` fetches `p9` from memory and puts its value into x2 (where it becomes the third argument to `printf()`).
|
This means that `Line 8` fetches `p9` from memory and puts its value
|
||||||
|
into x2 (where it becomes the third argument to `printf()`).
|
||||||
|
|
||||||
|
## A bit of history
|
||||||
|
|
||||||
|
The early Unix kernels would abuse the calling convention to
|
||||||
|
miraculously pass return values back to calling functions. Early
|
||||||
|
versions of C made extensive use of a now obsolete keyword `register`.
|
||||||
|
It was an instruction to the compiler to store a certain variable in
|
||||||
|
a register and not in memory in the code the compiler produced.
|
||||||
|
|
||||||
|
Particularly abusive functions would call other functions without
|
||||||
|
passing any actual variables but the parameters would indeed be passed!
|
||||||
|
The coders assumed the compiler would store specific variables in
|
||||||
|
specific registers, avoiding the overhead of using the actual calling
|
||||||
|
convention they themselves defined. Code that did this had to be
|
||||||
|
rewritten once Unix began to be ported to machines beyond the original
|
||||||
|
DEC hardware.
|
||||||
|
|
||||||
|
This had the author scratching his head until he figured it out, way
|
||||||
|
way back in the day.
|
||||||
|
|
||||||
|
Those were the days when the entire Unix kernel would be printed out to
|
||||||
|
form a stack of paper less than an inch high. The author knows this
|
||||||
|
because [Jishnu
|
||||||
|
Mukerji](<https://www.topionetworks.com/people/jishnu-mukerji-5288f3c41dedae1c01000582>)
|
||||||
|
presented such a stack to the author the third time the author asked
|
||||||
|
Jishnu a question about the kernel. He gave the author answers to two
|
||||||
|
questions. On the third question, he handed the author the print out and
|
||||||
|
said: *"All your answers are in here."* The author deeply appreciates
|
||||||
|
Jishnu Mukerji's formative impact on a young undergraduate.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue