mirror of
https://github.com/pkivolowitz/asm_book.git
synced 2026-06-21 04:06:48 +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
|
||||
|
||||
Calling functions, passing parameters to them and receiving back return
|
||||
values is basic to using `C` and and `C++`. Calling methods (which are
|
||||
functions connected to objects) is similar but with enough differences
|
||||
to warrant its own discussion to be provided later in the chapter on
|
||||
[structs](../structs/using.md).
|
||||
values is basic to using `C` and and `C++`.
|
||||
|
||||
Calling *methods* (which are functions connected to objects) is similar
|
||||
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
|
||||
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
|
||||
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
|
||||
a value.
|
||||
a value.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
If your functions call *any* other functions, `x30` must be backed
|
||||
|
|
|
|||
|
|
@ -1,12 +1,30 @@
|
|||
# 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:
|
||||
|
||||
|
|
@ -24,83 +42,34 @@ func: add x0, x0, x1 // 1
|
|||
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.
|
||||
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`.
|
||||
Just as scratch registers are used for passing (up to 8) parameters, the
|
||||
`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.
|
||||
|
||||
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`
|
||||
## `const`
|
||||
|
||||
Suppose we had:
|
||||
|
||||
|
|
@ -115,7 +84,105 @@ how would the assembly language change?
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -130,22 +197,56 @@ long func(long & p1, long & p2) // 1
|
|||
|
||||
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?
|
||||
|
||||
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 17 bytes of stack, the stack pointer must be changed by 32 and so on.
|
||||
* If you need just 1 byte of stack, the stack pointer must be changed by
|
||||
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++
|
||||
#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:
|
||||
|
||||
|
|
@ -233,4 +337,34 @@ sp + 32 return address for main
|
|||
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