Execute sequentially
Sequential execution is a relatively familiar working model, similar to the commonly known as turnover programming. All Go functions that do not contain branches, loops, and goto languages, and each recursively called Go function is generally executed sequentially.
For example, there are codes executed in the following order:
func main() { var a = 10 println(a) var b = (a+a)*a println(b) }
We try to rewrite the above function using the thinking of Go assembly. Because there are generally only 2 operands in the X86 instruction, there can only be at most one operator in the variable expression required to appear when rewriting in assembly. At the same time, for some function calls, you also need to use functions that can be called in assembly to rewrite.
The first step of rewriting is still to use Go language, but to rewrite it with assembly thinking:
func main() { var a, b int a = 10 (a) () b = a b += b b *= a (b) () }
First, imitate the C language processing method to declare all local variables at the function entrance. Then, according to the style of instructions such as MOV, ADD, MUL, etc., expand the previous variable expression as=
、+=
and*=
Several instructions expressed by several operations. Finally, use the printint and printnl functions inside the runtime package to replace the previous println function output result.
After rewritten with assembly thinking, although the above Go function seems a little more complicated, it is still relatively easy to understand. Below we further try to continue to translate the rewritten function into assembly function:
TEXT ·main(SB), $24-0
MOVQ $0, a-8*2(SP) // a = 0
MOVQ $0, b-8*1(SP) // b = 0// Write the new value to the corresponding memory of a
MOVQ $10, AX // AX = 10
MOVQ AX, a-8*2(SP) // a = AX// Call the function with a as parameter
MOVQ AX, 0(SP)
CALL runtime·printint
CALL runtime·printnl// After function calls, AX/BX may be contaminated and needs to be reloaded
MOVQ a-8*2(SP), AX // AX = a
MOVQ b-8*1(SP), BX // BX = b// Calculate the b value and write it to memory
MOVQ AX, BX // BX = AX // b = a
ADDQ BX, BX // BX += BX // b += a
MULQ AX, BX // BX *= AX // b *= a
MOVQ BX, b-8*1(SP) // b = BX// Call the function with b as the parameter
MOVQ BX, 0(SP)
CALL runtime·printint
CALL runtime·printnlRET
The first step in assembling the main function is to calculate the size of the function stack frame. Because the function has two int type variables a and b, and the runtime·printint function parameter called at the same time is an int type and has no return value, the stack frame of the main function is a 24-byte stack memory space composed of 3 int types.
First initialize the variable to a 0 value at the beginning of the function, wherea-8*2(SP)
Corresponding to a variable,a-8*1(SP)
Correspond to the b variable (because the a variable is defined first, so the address of the a variable is smaller).
Then a variable a is assigned an AX register, and the memory corresponding to a variable a is set to 10 through the AX register, and AX is also 10. In order to output the a variable, the value of the AX register needs to be put into0(SP)
Position, the variable at this position will be printed as its argument when the runtime·printint function is called. Because we have saved the AX value to the a variable memory before, we do not need to backup the registers before calling the function.
After the call function returns, all registers will be treated as the called function modification, so we need to restore registers AX and BX from the corresponding memory of a and b. Then refer to the calculation method of the b variable in Go language above to update the corresponding value of BX. After the calculation is completed, the value of BX will also be written to the corresponding memory of b.
Finally, use the b variable as parameter to call the runtime·printint function again for output work. All registers may also be contaminated, but main will return the registers such as AX and BX immediately, so there is no need to restore the register value again.
Re-analyzing the entire function after assembly and rewriting will reveal a lot of redundant code inside. We do not need the two temporary variables a and b to allocate two memory spaces, and we do not need to write to memory after each register changes. Here is the optimized assembly function:
TEXT ·main(SB), $16-0
// var temp int// Write the new value to the corresponding memory of a
MOVQ $10, AX // AX = 10
MOVQ AX, temp-8(SP) // temp = AX// Call the function with a as parameter
CALL runtime·printint
CALL runtime·printnl// After function calls, AX may be polluted and needs to be reloaded
MOVQ temp-8*1(SP), AX // AX = temp// Calculate the b value, no need to write to memory
MOVQ AX, BX // BX = AX // b = a
ADDQ BX, BX // BX += BX // b += a
MULQ AX, BX // BX *= AX // b *= a// ...
The first is to reduce the stack frame size of the main function from 24 bytes to 16 bytes. The only thing that needs to be saved is the value of the a variable, so when calling the runtime·printint function output, all registers may be contaminated. We cannot backup the value of the a variable through the registers, and only the values in the stack memory are safe. Then the BX register does not need to be saved to memory. The code in other parts remains basically the same.
if/goto jump
Although the early Go provided goto statements, it was not recommended to use in programming. There is a principle similar to cgo: if you can not use goto statements, then don't use goto statements. Goto statements in Go are strictly limited: they cannot span code blocks, and they cannot contain variable definition statements in the code being spanned. Although Go doesn't like goto, goto is indeed a favorite of every assembly language coder. goto is approximately equivalent to the unconditional jump instruction JMP in assembly language. Combined with if condition goto, a conditional jump instruction is formed, and the conditional jump instruction is the cornerstone of building the entire assembly code control flow.
For easy understanding, we use Go to construct an If function that simulates ternary expressions:
func If(ok bool, a, b int) int { if ok { return a } else { return b } }
For example, find the maximum value of two numbers(a>b)?a:b
Use the If function to express it like this:If(a>b, a, b)
. Due to language limitations, the If function used to simulate ternary expressions does not support the normal type (a, b and return types can be changed to empty interfaces, which will be more cumbersome).
Although this function seems to have only a simple line, it contains if branch statements. Before using assembly to implement it, we should use assembly thinking to rewrite If function first. When rewriting, we must also follow the limitation that each expression can only have one operator. At the same time, the conditional part of the if statement must be composed of only one comparison symbol, and the body part of the if statement can only be one goto statement.
The If function rewritten using assembly thinking is implemented as follows:
func If(ok int, a, b int) int { if ok == 0 { goto L } return a L: return b }
Because there is no bool type in assembly language, we use the int type instead of bool type (the real assembly uses byte to represent bool type, and the value of byte type can be loaded through the MOVBQZX instruction). Return variable a when the ok parameter is not 0, otherwise return variable b. We reverse the logic of ok: When the ok parameter is 0, it means that b is returned, otherwise the variable a is returned. In the if statement, when the ok parameter is 0, goto to the statement specified by the L label, that is, return the variable b. If the if condition is not satisfied, that is, ok is not 0, execute the following statement to return the variable a.
The implementation of the above functions is very close to assembly language. Here is the code that changed to assembly implementation:
TEXT ·If(SB), NOSPLIT, $0-32
MOVQ ok+8*0(FP), CX // ok
MOVQ a+8*1(FP), AX // a
MOVQ b+8*2(FP), BX // bCMPQ CX, $0 // test ok
JZ L // if ok == 0, skip 2 line
MOVQ AX, ret+24(FP) // return a
RETL:
MOVQ BX, ret+24(FP) // return b
RET
First, load three parameters into the register. The ok parameter corresponds to the CX register, and a and b corresponds to the AX and BX registers respectively. Then use the CMPQ comparison instruction to compare the CX register with the constant 0. If the result of the comparison is 0, then the jump instruction will jump to the instruction corresponding to the L label when the next JZ is 0, that is, the value of the variable b is returned. If the comparison result is not 0, then the JZ instruction has no effect. The instruction after execution continues, that is, the value of variable a is returned.
In the jump command, the jump target is generally indicated by a label. However, in some functions implemented through macros, it is more likely to jump through relative positions. At this time, the jump position can be calculated through the PC register.
for loop
There are many uses of the for loop in Go language, and we will only choose the most classic for structure to discuss here. The classic for loop consists of three parts: initialization, ending conditions, and iteration step length, and combined with the if conditional language inside the loop body, this for structure can simulate various other loop types.
Based on the classic for loop structure, we define a LoopAdd function, which can be used to calculate the sum of any arithmetic sequence:
func LoopAdd(cnt, v0, step int) int { result := v0 for i := 0; i < cnt; i++ { result += step } return result }
for example1+2+...+100
This can be calculatedLoopAdd(100, 1, 1)
,10+8+...+0
This can be calculatedLoopAdd(5, 10, -2)
. Now adopt the frontif/goto
Similar techniques to transform the for loop.
The new LoopAdd function only consists of if/goto statement:
func LoopAdd(cnt, v0, step int) int { var i = 0 var result = 0 LOOP_BEGIN: result = v0 LOOP_IF: if i < cnt { goto LOOP_BODY } goto LOOP_END LOOP_BODY i = i+1 result = result + step goto LOOP_IF LOOP_END: return result }
The beginning of the function defines two local variables first for subsequent code use. Then the three parts of the for statement are initialized, ended conditions, and iteration step length divided into three code segments, which are represented by three labels: LOOP_BEGIN, LOOP_IF, and LOOP_BODY. The LOOP_BEGIN loop initialization part will only be executed once, so this label will not be referenced and can be omitted. The last LOOP_END statement indicates the end of the for loop. The three code segments separated by four labels correspond to the initialization statement, loop conditions and loop body of the for loop, and the iterative statement is merged into the loop body.
The following is to re-implement the LoopAdd function in assembly language
// func LoopAdd(cnt, v0, step int) int TEXT ·LoopAdd(SB), NOSPLIT, $0-32 MOVQ cnt+0(FP), AX // cnt MOVQ v0+8(FP), BX // v0/result MOVQ step+16(FP), CX // step LOOP_BEGIN: MOVQ $0, DX // i LOOP_IF: CMPQ DX, AX // compare i, cnt JL LOOP_BODY // if i < cnt: goto LOOP_BODY goto LOOP_END LOOP_BODY: ADDQ $1, DX // i++ ADDQ CX, BX // result += step goto LOOP_IF LOOP_END: MOVQ BX, ret+24(FP) // return result RET
The v0 and result variables multiplex a BX register. In the instruction part corresponding to the LOOP_BEGIN label, use MOVQ to initialize the DX register to 0, and DX corresponds to the variable i, and the iterative variable of the loop. In the instruction part corresponding to the LOOP_IF label, use the CMPQ instruction to compare AX and AX. If the loop does not end, it will jump to the LOOP_BODY part, otherwise it will jump to the LOOP_END part to end the loop. In the LOOP_BODY part, update the iterative variable and execute the accumulated statement in the loop body, and then jump directly to the LOOP_IF part to enter the next round of loop condition judgment. After the LOOP_END number, return the accumulated result to the statement.
Loops are the most complex control flow, and branches and jump statements are implicit in the loop. If you master the loop, you will basically master the assembly language to writing. After mastering the rules, assembly language programming will actually become extremely simple.
This is the article about this in-depth analysis and explanation of Golang assembly control flow. For more related Go language assembly control flow content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!