go 通过汇编分析函数传参与返回值机制
文章目录
- 概要
- 一、前置知识
- 二、汇编分析
- 2.1、示例
- 2.2、汇编
- 2.2.1、 寄存器传值的汇编
- 2.2.2、 栈内存传值的汇编
- 三、拓展
- 3.1 了解go中的Duff’s Device
- 3.2 go tool compile
- 3.2 call 0x46dc70 & call 0x46dfda
概要
在上一篇文章中,我们研究了go函数调用时的栈布局,观察到了其函数参数和返回值一般通过AX、BX等通用寄存器传递,但是函数示例的传参和返回值个数都很少,自然会想到,如果函数参数和返回值很多,怎么办?毕竟CPU的寄存器数量是有限的,而go函数参数数量可没限制。
调试环境:Centos Linux 7 ,CPU AMD x86_64,Go version 1.24
先说结论:当函数参数和返回值过多时,多余的值直接通过栈来传递,下面就让我们通过go汇编来验证下吧。
一、前置知识
AMD x86_64 CPU 常见寄存器:
另外还有8个专为Steraming SIMD Extensions(SSE——多指令多数据流扩展)准备的寄存器(128位):xmm0~xmm15。
对于AX系列寄存器,在16位下是ax、32位下是eax、64位下是rax,如果在64位cpu下使用eax,表示只用该寄存器前32位,其他同理。
二、汇编分析
2.1、示例
1 package main23 func main() { // 探索栈布局4 x := int64(6)5 callMaxTest(x)6 rv := returnMax()7 x = rv.Id58 }910 func callMaxTest(z int64) {11 d := MaxData{12 Id: 1,13 Id1: 2,14 Id2: 3,15 Id3: z,16 Id4: 5,17 Id5: 6,18 Id6: 7,19 Id7: 8,20 Id8: 9,21 }22 d.Id = callMax(d) //过大的传参数据,寄存器不够用,所以会用过栈内存传递数据23 }2425 func callMax(md MaxData) int64 {26 a, b := md.Id2, md.Id527 return a + b28 }2930 func returnMax() MaxData { //过大的返回数据,寄存器不够用,所以会用过栈内存传递数据31 y := int64(7)32 md := MaxData{33 Id: 1,34 Id1: 2,35 Id2: 3,36 Id3: 4,37 Id4: 5,38 Id5: 6,39 Id6: 7,40 Id7: 8,41 Id8: 9,42 }43 md.Id5 = y44 return md45 }4647 type MaxData struct { //x86_6448 Id int64 //AX49 Id1 int64 //BX50 Id2 int64 //CX51 Id3 int64 //DI52 Id4 int64 //SI53 Id5 int64 //R854 Id6 int64 //R955 Id7 int64 //R1056 Id8 int64 //R1157 //Id9 int64 //导致寄存器不足, 编译器判定使用栈内存传递数据58 }
通过调整MaxData结构体字段个数,得到9个字段是是否使用寄存器传参的极限。
2.2、汇编
通过寄存器传值和栈内存传值两种模式分析。
仍然使用dlv debug 来调试。
2.2.1、 寄存器传值的汇编
即MaxData结构体字段个数是9个的情况下的汇编,由于本文只分析函数传参和返回值,所以我们只研究callMaxTest和returnMax的汇编。
TEXT main.callMaxTest(SB) /home/gofunc/ret.goret.go:10 0x470be0 4c8d6424e0 lea r12, ptr [rsp-0x20]ret.go:10 0x470be5 4d3b6610 cmp r12, qword ptr [r14+0x10]ret.go:10 0x470be9 0f86c0000000 jbe 0x470cafret.go:10 0x470bef 55 push rbpret.go:10 0x470bf0 4889e5 mov rbp, rsp
=> ret.go:10 0x470bf3* 4881ec98000000 sub rsp, 0x98ret.go:10 0x470bfa 48898424a8000000 mov qword ptr [rsp+0xa8], rax#令rsp+0xa8地址保存参数zret.go:20 0x470c02 48c744244801000000 mov qword ptr [rsp+0x48], 0x1#开始设置各个字段的值,令字段Id=1ret.go:20 0x470c0b 48c744245002000000 mov qword ptr [rsp+0x50], 0x2ret.go:20 0x470c14 48c744245803000000 mov qword ptr [rsp+0x58], 0x3ret.go:20 0x470c1d 48c744246000000000 mov qword ptr [rsp+0x60], 0x0#令字段Id3=0ret.go:20 0x470c26 48c744246805000000 mov qword ptr [rsp+0x68], 0x5ret.go:20 0x470c2f 48c744247006000000 mov qword ptr [rsp+0x70], 0x6ret.go:20 0x470c38 48c744247807000000 mov qword ptr [rsp+0x78], 0x7ret.go:20 0x470c41 48c784248000000008000000 mov qword ptr [rsp+0x80], 0x8ret.go:20 0x470c4d 48c784248800000009000000 mov qword ptr [rsp+0x88], 0x9#令字段Id8=9ret.go:15 0x470c59 488bbc24a8000000 mov rdi, qword ptr [rsp+0xa8]#开始设置参数,令RDX传递字段Id3的值ret.go:15 0x470c61 48897c2460 mov qword ptr [rsp+0x60], rdi#令字段Id3等于参数zret.go:22 0x470c66 488b442448 mov rax, qword ptr [rsp+0x48]#令RAX传递字段Id的值ret.go:22 0x470c6b 488b5c2450 mov rbx, qword ptr [rsp+0x50]ret.go:22 0x470c70 488b4c2458 mov rcx, qword ptr [rsp+0x58]ret.go:22 0x470c75 488b742468 mov rsi, qword ptr [rsp+0x68]ret.go:22 0x470c7a 4c8b442470 mov r8, qword ptr [rsp+0x70]ret.go:22 0x470c7f 4c8b4c2478 mov r9, qword ptr [rsp+0x78]ret.go:22 0x470c84 4c8b942480000000 mov r10, qword ptr [rsp+0x80]ret.go:22 0x470c8c 4c8b9c2488000000 mov r11, qword ptr [rsp+0x88]#令R11传递字段Id8的值ret.go:22 0x470c94 e847000000 call $main.callMaxret.go:22 0x470c99 4889842490000000 mov qword ptr [rsp+0x90], raxret.go:22 0x470ca1 4889442448 mov qword ptr [rsp+0x48], raxret.go:23 0x470ca6 4881c498000000 add rsp, 0x98ret.go:23 0x470cad 5d pop rbpret.go:23 0x470cae c3 retret.go:10 0x470caf 4889442408 mov qword ptr [rsp+0x8], raxret.go:10 0x470cb4 e847acffff call $runtime.morestack_noctxtret.go:10 0x470cb9 488b442408 mov rax, qword ptr [rsp+0x8]ret.go:10 0x470cbe 6690 data16 nopret.go:10 0x470cc0 e91bffffff jmp $main.callMaxTest
TEXT main.returnMax(SB) /home/gofunc/ret.goret.go:30 0x470d40 4c8d6424e0 lea r12, ptr [rsp-0x20]ret.go:30 0x470d45 4d3b6610 cmp r12, qword ptr [r14+0x10]ret.go:30 0x470d49 0f86fe000000 jbe 0x470e4dret.go:30 0x470d4f 55 push rbpret.go:30 0x470d50 4889e5 mov rbp, rsp
=> ret.go:30 0x470d53* 4881ec98000000 sub rsp, 0x98ret.go:30 0x470d5a 440f113c24 movups xmmword ptr [rsp], xmm15#令rsp+0x0地址值等于0ret.go:30 0x470d5f 440f117c2408 movups xmmword ptr [rsp+0x8], xmm15ret.go:30 0x470d65 440f117c2418 movups xmmword ptr [rsp+0x18], xmm15ret.go:30 0x470d6b 440f117c2428 movups xmmword ptr [rsp+0x28], xmm15ret.go:30 0x470d71 440f117c2438 movups xmmword ptr [rsp+0x38], xmm15ret.go:31 0x470d77 48c744244807000000 mov qword ptr [rsp+0x48], 0x7#令变量y=7ret.go:41 0x470d80 48c744245001000000 mov qword ptr [rsp+0x50], 0x1#开始设置各个字段的值,令字段Id=1ret.go:41 0x470d89 48c744245802000000 mov qword ptr [rsp+0x58], 0x2ret.go:41 0x470d92 48c744246003000000 mov qword ptr [rsp+0x60], 0x3ret.go:41 0x470d9b 48c744246804000000 mov qword ptr [rsp+0x68], 0x4ret.go:41 0x470da4 48c744247005000000 mov qword ptr [rsp+0x70], 0x5ret.go:41 0x470dad 48c744247806000000 mov qword ptr [rsp+0x78], 0x6#令字段Id5=6ret.go:41 0x470db6 48c784248000000007000000 mov qword ptr [rsp+0x80], 0x7ret.go:41 0x470dc2 48c784248800000008000000 mov qword ptr [rsp+0x88], 0x8ret.go:41 0x470dce 48c784249000000009000000 mov qword ptr [rsp+0x90], 0x9#令字段Id8=9ret.go:43 0x470dda 488b542448 mov rdx, qword ptr [rsp+0x48]#令RDX等于变量yret.go:43 0x470ddf 4889542478 mov qword ptr [rsp+0x78], rdx#令字段Id5等于RDX,即等于变量yret.go:44 0x470de4 488b542450 mov rdx, qword ptr [rsp+0x50]#令RDX等于字段Idret.go:44 0x470de9 48891424 mov qword ptr [rsp], rdx#令rsp地址等于RDX,即等于字段Idret.go:44 0x470ded 0f10442458 movups xmm0, xmmword ptr [rsp+0x58] #将XMM0寄存器(16字节)等于rsp+0x58地址到rsp+0x58+16字节地址之间的内容,即将字段Id1和字段Id2的值设给XMM0寄存器ret.go:44 0x470df2 0f11442408 movups xmmword ptr [rsp+0x8], xmm0#将字段Id1和字段Id2的值依次设置给地址rsp+0x8 和 rsp+0x10ret.go:44 0x470df7 0f10442468 movups xmm0, xmmword ptr [rsp+0x68]ret.go:44 0x470dfc 0f11442418 movups xmmword ptr [rsp+0x18], xmm0ret.go:44 0x470e01 0f10442478 movups xmm0, xmmword ptr [rsp+0x78]ret.go:44 0x470e06 0f11442428 movups xmmword ptr [rsp+0x28], xmm0ret.go:44 0x470e0b 0f10842488000000 movups xmm0, xmmword ptr [rsp+0x88]ret.go:44 0x470e13 0f11442438 movups xmmword ptr [rsp+0x38], xmm0ret.go:44 0x470e18 488b0424 mov rax, qword ptr [rsp]#开始处理返回值,RAX等于rsp地址,即等于字段Id, 则是令RAX返回字段Id的值ret.go:44 0x470e1c 488b5c2408 mov rbx, qword ptr [rsp+0x8]#RBX 返回字段Id1的值ret.go:44 0x470e21 488b4c2410 mov rcx, qword ptr [rsp+0x10]#RCX 返回字段Id2的值ret.go:44 0x470e26 488b7c2418 mov rdi, qword ptr [rsp+0x18]ret.go:44 0x470e2b 488b742420 mov rsi, qword ptr [rsp+0x20]ret.go:44 0x470e30 4c8b442428 mov r8, qword ptr [rsp+0x28]ret.go:44 0x470e35 4c8b4c2430 mov r9, qword ptr [rsp+0x30]ret.go:44 0x470e3a 4c8b542438 mov r10, qword ptr [rsp+0x38]ret.go:44 0x470e3f 4c8b5c2440 mov r11, qword ptr [rsp+0x40]#R11 返回字段Id8的值ret.go:44 0x470e44 4881c498000000 add rsp, 0x98ret.go:44 0x470e4b 5d pop rbpret.go:44 0x470e4c c3 retret.go:30 0x470e4d e8aeaaffff call $runtime.morestack_noctxtret.go:30 0x470e52 e9e9feffff jmp $main.returnMax
可以看到寄存器RAX、RBX、RCX、RDI、RSI、R8、R9、R10、R11这9个寄存器都参与了函数参数和返回值的传递。
2.2.2、 栈内存传值的汇编
此时需要将结构体MaxData的Id9字段放出来,即10个字段时可以触发函数之间通过栈内存传值,参数和返回值在函数之间的传递原理一样的,所以这里只分析returnMax返回值的传递。
TEXT main.main(SB) /home/gofunc/ret.goret.go:3 0x470b00 4c8d6424d0 lea r12, ptr [rsp-0x30]ret.go:3 0x470b05 4d3b6610 cmp r12, qword ptr [r14+0x10]ret.go:3 0x470b09 765e jbe 0x470b69ret.go:3 0x470b0b 55 push rbpret.go:3 0x470b0c 4889e5 mov rbp, rsp
=> ret.go:3 0x470b0f* 4881eca8000000 sub rsp, 0xa8ret.go:4 0x470b16 48c744245006000000 mov qword ptr [rsp+0x50], 0x6#设置变量x=6ret.go:5 0x470b1f b806000000 mov eax, 0x6 #设置EAX等与变量x的值6,用于调用callMaxTest函数传参ret.go:5 0x470b24 e857000000 call $main.callMaxTestret.go:6 0x470b29 e852010000 call $main.returnMax#调用returnMax函数ret.go:6 0x470b2e 488d7c2458 lea rdi, ptr [rsp+0x58]ret.go:6 0x470b33 4889e6 mov rsi, rspret.go:6 0x470b36 660f1f840000000000 nop word ptr [rax+rax*1], axret.go:6 0x470b3f 90 nopret.go:6 0x470b40 48896c24f0 mov qword ptr [rsp-0x10], rbpret.go:6 0x470b45 488d6c24f0 lea rbp, ptr [rsp-0x10]ret.go:6 0x470b4a e88bd4ffff call 0x46dfda#跳到0x46dfda地址处,执行相应机器码,作用是将rsp+0x8到rsp+0x8+80字节(rsp+0x58)地址之间的内容依次复制到rsp+0x58到rsp+0x58+80字节之间地址上。结合returnMax逻辑,可知,main函数栈rsp+0x58到rsp+0x58+80字节(rsp+0xa8)地址之间的栈内存用于保存returnMax函数返回值。rsp+0x58 = rv.Id,rsp+0x60 = rv.Id1 依次类推。ret.go:6 0x470b4f 488b6d00 mov rbp, qword ptr [rbp]ret.go:7 0x470b53 488b8c2480000000 mov rcx, qword ptr [rsp+0x80]#取rv.Id5值放到RCXret.go:7 0x470b5b 48894c2450 mov qword ptr [rsp+0x50], rcx#令x=rv.Id5ret.go:8 0x470b60 4881c4a8000000 add rsp, 0xa8ret.go:8 0x470b67 5d pop rbpret.go:8 0x470b68 c3 retret.go:3 0x470b69 e892adffff call $runtime.morestack_noctxtret.go:3 0x470b6e eb90 jmp $main.main
TEXT main.returnMax(SB) /home/gofunc/ret.goret.go:30 0x470c80 55 push rbpret.go:30 0x470c81 4889e5 mov rbp, rsp
=> ret.go:30 0x470c84* 4883ec58 sub rsp, 0x58ret.go:30 0x470c88 488d7c2468 lea rdi, ptr [rsp+0x68]ret.go:30 0x470c8d 488d7fd0 lea rdi, ptr [rdi-0x30]#给call 0x46dc70对应函数用的ret.go:30 0x470c91 660f1f840000000000 nop word ptr [rax+rax*1], axret.go:30 0x470c9a 660f1f440000 nop word ptr [rax+rax*1], axret.go:30 0x470ca0 48896c24f0 mov qword ptr [rsp-0x10], rbpret.go:30 0x470ca5 488d6c24f0 lea rbp, ptr [rsp-0x10]ret.go:30 0x470caa e8c1cfffff call 0x46dc70 #跳到0x46dc70地址处,执行相应机器码,作用是将rsp+0x68 到rsp+0x68+80字节(MaxData结构体大小)之间地址置为0ret.go:30 0x470caf 488b6d00 mov rbp, qword ptr [rbp]ret.go:31 0x470cb3 48c7042407000000 mov qword ptr [rsp], 0x7 #令变量y=7ret.go:41 0x470cbb 488d7c2408 lea rdi, ptr [rsp+0x8]#给call 0x46dfda函数用的ret.go:41 0x470cc0 488d35b9dd0100 lea rsi, ptr [rip+0x1ddb9]#给call 0x46dfda函数用的ret.go:41 0x470cc7 48896c24f0 mov qword ptr [rsp-0x10], rbpret.go:41 0x470ccc 488d6c24f0 lea rbp, ptr [rsp-0x10]ret.go:41 0x470cd1 e804d3ffff call 0x46dfda#跳到0x46dfda地址处,执行相应机器码,作用是将rip+0x1ddb9到rip+0x1ddb98+80字节地址之间的内容依次复制到rsp+0x8到rsp+0x8+80字节之间地址上。即将变量md的值从数据区复制到returnMax函数的栈上。ret.go:41 0x470cd6 488b6d00 mov rbp, qword ptr [rbp]ret.go:43 0x470cda 488b0424 mov rax, qword ptr [rsp]#令RAX等于变量yret.go:43 0x470cde 4889442430 mov qword ptr [rsp+0x30], rax#令md.Id5=yret.go:44 0x470ce3 488d7c2468 lea rdi, ptr [rsp+0x68]#rsp+0x60是main函数的栈顶ret.go:44 0x470ce8 488d742408 lea rsi, ptr [rsp+0x8]ret.go:44 0x470ced 660f1f840000000000 nop word ptr [rax+rax*1], axret.go:44 0x470cf6 660f1f840000000000 nop word ptr [rax+rax*1], axret.go:44 0x470cff 90 nopret.go:44 0x470d00 48896c24f0 mov qword ptr [rsp-0x10], rbpret.go:44 0x470d05 488d6c24f0 lea rbp, ptr [rsp-0x10]ret.go:44 0x470d0a e8cbd2ffff call 0x46dfda#跳到0x46dfda地址处,执行相应机器码,作用是将rsp+0x8到rsp+0x8+80字节地址之间的内容依次复制到rsp+0x68到rsp+0x68+80字节之间地址上。即将变量md的值从returnMax函数的栈复制到main函数栈上,实现栈内存传递值。ret.go:44 0x470d0f 488b6d00 mov rbp, qword ptr [rbp]ret.go:44 0x470d13 4883c458 add rsp, 0x58ret.go:44 0x470d17 5d pop rbpret.go:44 0x470d18 c3 ret
看到这里读者就清晰的知道栈内存传值的原理了,就是caller开辟栈空间(参数和返回值都是caller开辟)时专门多申请一部分内存接收callee的返回值,callee执行时会将返回值直接写入到caller栈上,朴素而又好用吧。
假设main函数的BP是0x3f0(1008),则main和returnMax的栈结构如下(注意此时MaxData大小是80字节):
三、拓展
在2.2.2小节,可以看到我对call 0x46dc70和call 0x46dfda两个汇编指令一长串的注释,怎么得来的呢?
3.1 了解go中的Duff’s Device
Duff’s Device就是将循环展开,减少判断次数来提升性能的一种机制。
在runtime/mkduff.go中,可以看到在amd x86_64下的两个Duff函数:runtime.duffzero() 【高效将某段连续内存清零】和runtime·duffcopy【高效将某段连续内存内容复制到另一段内存中】
func zeroAMD64(w io.Writer) {// X15: zero// DI: ptr to memory to be zeroed 通过DI寄存器确定要清零内存得低地址的一侧起始地址// DI is updated as a side effect.fmt.Fprintln(w, "TEXT runtime·duffzero<ABIInternal>(SB), NOSPLIT|NOFRAME, $0-0")for i := 0; i < 16; i++ {fmt.Fprintln(w, "\tMOVUPS\tX15,(DI)")fmt.Fprintln(w, "\tMOVUPS\tX15,16(DI)")fmt.Fprintln(w, "\tMOVUPS\tX15,32(DI)")fmt.Fprintln(w, "\tMOVUPS\tX15,48(DI)")fmt.Fprintln(w, "\tLEAQ\t64(DI),DI") // We use lea instead of add, to avoid clobbering flagsfmt.Fprintln(w)}fmt.Fprintln(w, "\tRET")
}
func copyAMD64(w io.Writer) {// SI: ptr to source memory SI寄存器指向源内存地址// DI: ptr to destination memory DI寄存器指向目的内存地址// SI and DI are updated as a side effect.// This is equivalent to a sequence of MOVSQ but for some reason that is 3.5x slower than this code.fmt.Fprintln(w, "TEXT runtime·duffcopy<ABIInternal>(SB), NOSPLIT|NOFRAME, $0-0")for i := 0; i < 64; i++ {fmt.Fprintln(w, "\tMOVUPS\t(SI), X0")fmt.Fprintln(w, "\tADDQ\t$16, SI")fmt.Fprintln(w, "\tMOVUPS\tX0, (DI)")fmt.Fprintln(w, "\tADDQ\t$16, DI")fmt.Fprintln(w)}fmt.Fprintln(w, "\tRET")
}
具体内容可以到runtime/duff_amd64.s文件中看对应汇编内容。
runtime·duffzero就是16个
MOVUPS X15,(DI) #4字节 [44 0f 11 3f] 汇编对应的机器码
MOVUPS X15,16(DI) #5字节 [44 0f 11 7f 10]
MOVUPS X15,32(DI) #5字节 [44 0f 11 7f 20]
MOVUPS X15,48(DI) #5字节 [44 0f 11 7f 30]
LEAQ 64(DI),DI #4字节 [48 8d 7f 40]
依次展开,一次循环的指令是23字节,留意这个数字。
runtime·duffcopy就是64个
MOVUPS (SI), X0 #3字节
ADDQ $16, SI #4字节
MOVUPS X0, (DI) #3字节
ADDQ $16, DI #4字节
依次展开,一次循环的指令是14字节,留意这个数字。
3.2 go tool compile
我们go tool compile -S -N -l ret.go
看看其plan9汇编,选取returnMax函数关键部分,与2.2.2小节returnMax函数汇编对比,可以发现
call 0x46dc70 对应 DUFFZERO $336
call 0x46dfda 对应 DUFFCOPY $826
lea rsi, ptr [rip+0x1ddb9] 对应 LEAQ main…stmp_1(SB), SI
那么[DUFFZERO $336]表示跳到在代码区相对runtime·duffzero第一个机器码偏移336个字节的机器码;
那么[DUFFCOPY $826]表示跳到在代码区相对runtime·duffcopy第一个机器码偏移826个字节的机器码;
程序内存一般分为代码区(程序编译后的机器码)、数据区(常量、全局变量等)、堆区、栈区。
main.returnMax STEXT nosplit size=153 args=0x50 locals=0x60 funcid=0x0 align=0x0#...省略0x000d 00013 (/home/gofunc/ret.go:30) LEAQ -48(DI), DI0x0011 00017 (/home/gofunc/ret.go:30) NOP0x0020 00032 (/home/gofunc/ret.go:30) DUFFZERO $3360x0033 00051 (/home/gofunc/ret.go:31) PCDATA $0, $-10x0033 00051 (/home/gofunc/ret.go:31) MOVQ $7, main.y(SP)0x003b 00059 (/home/gofunc/ret.go:41) LEAQ main.md+8(SP), DI0x0040 00064 (/home/gofunc/ret.go:41) LEAQ main..stmp_1(SB), SI0x0047 00071 (/home/gofunc/ret.go:41) PCDATA $0, $-20x0047 00071 (/home/gofunc/ret.go:41) DUFFCOPY $826#...省略 0x0098 00152 (/home/gofunc/ret.go:44) RET
main..stmp_1 SRODATA static size=80 #数据区,returnMax函数变量md对应的值(只读,80字节)0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................0x0020 05 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 ................0x0030 07 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 ................0x0040 09 00 00 00 00 00 00 00
LEAQ main…stmp_1(SB), SI表示将在数据区存储的returnMax函数变量md对应的值的起始地址装在到SI寄存器中,即方便后面通过DUFFCOPY函数将数据拷贝到returnMax函数栈内存中。
在dlv debug ret.go中,可以通过si命令进入call 0x46dc70和call 0x46dfda的函数中。
3.2 call 0x46dc70 & call 0x46dfda
(dlv) si
> runtime.duffzero() /usr/local/go/src/runtime/duff_amd64.s:95 (PC: 0x46dc70)
Warning: debugging optimized functionduff_amd64.s:89 0x46dc59 440f117f30 movups xmmword ptr [rdi+0x30], xmm15duff_amd64.s:90 0x46dc5e 488d7f40 lea rdi, ptr [rdi+0x40]duff_amd64.s:92 0x46dc62 440f113f movups xmmword ptr [rdi], xmm15duff_amd64.s:93 0x46dc66 440f117f10 movups xmmword ptr [rdi+0x10], xmm15duff_amd64.s:94 0x46dc6b 440f117f20 movups xmmword ptr [rdi+0x20], xmm15
=> duff_amd64.s:95 0x46dc70 440f117f30 movups xmmword ptr [rdi+0x30], xmm15 #从本指令开始执行 ,这里在rdi+0x30 基础上操作的,也解释了2.2.2小节汇编中lea rdi, ptr [rsp+0x68]之后为啥强行来一个lea rdi, ptr [rsp-0x30]了duff_amd64.s:96 0x46dc75 488d7f40 lea rdi, ptr [rdi+0x40]duff_amd64.s:98 0x46dc79 440f113f movups xmmword ptr [rdi], xmm15duff_amd64.s:99 0x46dc7d 440f117f10 movups xmmword ptr [rdi+0x10], xmm15duff_amd64.s:100 0x46dc82 440f117f20 movups xmmword ptr [rdi+0x20], xmm15duff_amd64.s:101 0x46dc87 440f117f30 movups xmmword ptr [rdi+0x30], xmm15duff_amd64.s:102 0x46dc8c 488d7f40 lea rdi, ptr [rdi+0x40]duff_amd64.s:104 0x46dc90 c3 ret #函数执行结束
显示,call 0x46dc70是从runtime.duffzero()函数的中间某个位置开始执行的,即duff_amd64.s:95位置,为什么?
在3.1小节中知道runtime·duffzero一个循环的机器码是23字节,ret指令占1字节,那么runtime·duffzero函数的机器码是16*23+1=369字节,
在3.2小节中得知call 0x46dc70 对应 DUFFZERO $336,即偏移了336字节,则369-336=33字节,从duff_amd64.s:95位置的机器码开始算,到duff_amd64.s:104的机器码正好33字节,这下知道为什么从duff_amd64.s:95位置的机器码开始执行了吗?
我们在前置知识中了解到XMM系列寄存器是16字节,那么movups一次就操作16字节:
duff_amd64.s:95
duff_amd64.s:98
duff_amd64.s:99
duff_amd64.s:100
duff_amd64.s:101
这5次清零操作刚好是16*5=80字节,等于结构体MaxData大小了,都是go编译器精密计算好的。
【call 0x46dfda】汇编如下:
> runtime.duffcopy() /usr/local/go/src/runtime/duff_amd64.s:402 (PC: 0x46dfda)
Warning: debugging optimized functionduff_amd64.s:395 0x46dfc8 4883c710 add rdi, 0x10duff_amd64.s:397 0x46dfcc 0f1006 movups xmm0, xmmword ptr [rsi]duff_amd64.s:398 0x46dfcf 4883c610 add rsi, 0x10duff_amd64.s:399 0x46dfd3 0f1107 movups xmmword ptr [rdi], xmm0duff_amd64.s:400 0x46dfd6 4883c710 add rdi, 0x10
=> duff_amd64.s:402 0x46dfda 0f1006 movups xmm0, xmmword ptr [rsi] #从本机器码开始duff_amd64.s:403 0x46dfdd 4883c610 add rsi, 0x10duff_amd64.s:404 0x46dfe1 0f1107 movups xmmword ptr [rdi], xmm0duff_amd64.s:405 0x46dfe4 4883c710 add rdi, 0x10duff_amd64.s:407 0x46dfe8 0f1006 movups xmm0, xmmword ptr [rsi]duff_amd64.s:408 0x46dfeb 4883c610 add rsi, 0x10duff_amd64.s:409 0x46dfef 0f1107 movups xmmword ptr [rdi], xmm0duff_amd64.s:410 0x46dff2 4883c710 add rdi, 0x10duff_amd64.s:412 0x46dff6 0f1006 movups xmm0, xmmword ptr [rsi]duff_amd64.s:413 0x46dff9 4883c610 add rsi, 0x10duff_amd64.s:414 0x46dffd 0f1107 movups xmmword ptr [rdi], xmm0duff_amd64.s:415 0x46e000 4883c710 add rdi, 0x10duff_amd64.s:417 0x46e004 0f1006 movups xmm0, xmmword ptr [rsi]duff_amd64.s:418 0x46e007 4883c610 add rsi, 0x10duff_amd64.s:419 0x46e00b 0f1107 movups xmmword ptr [rdi], xmm0duff_amd64.s:420 0x46e00e 4883c710 add rdi, 0x10duff_amd64.s:422 0x46e012 0f1006 movups xmm0, xmmword ptr [rsi]duff_amd64.s:423 0x46e015 4883c610 add rsi, 0x10duff_amd64.s:424 0x46e019 0f1107 movups xmmword ptr [rdi], xmm0duff_amd64.s:425 0x46e01c 4883c710 add rdi, 0x10duff_amd64.s:427 0x46e020 c3 ret
有兴趣的朋友可以在评论区说出其偏移原理了。
相关文章:
go 通过汇编分析函数传参与返回值机制
文章目录 概要一、前置知识二、汇编分析2.1、示例2.2、汇编2.2.1、 寄存器传值的汇编2.2.2、 栈内存传值的汇编 三、拓展3.1 了解go中的Duff’s Device3.2 go tool compile3.2 call 0x46dc70 & call 0x46dfda 概要 在上一篇文章中,我们研究了go函数调用时的栈布…...
蓝桥杯C/C++省赛/国赛注意事项及运行环境配置
大佬的蓝桥杯考前急救指南 对拍(手动生成测试数据)代码: #include <bits/stdc.h> // 包含所有标准库的头文件 using namespace std; // 使用标准命名空间int main() {srand(time(0)); // 设置随机数种子为当前时间,确保每次…...
CSS高度坍塌?如何解决?
一、什么是高度坍塌? 高度坍塌(Collapsing Margins)是指当父元素没有设置边框(border)、内边距(padding)、内容(content)或清除浮动时,其子元素的 margin 会…...
redis的基本使用
简介 redis,Remote Dictionary Server,远程字典服务,一个基于内存的、存储键值对的数据库。redis是开源的,使用C语言编写。因为redis的数据是存储在内存中的,所以redis通常被用来做数据库的缓存 优点: re…...
【蓝桥杯】单片机设计与开发,第十二届
/*头文件声明区*/ #include <STC15F2K60S2.H>//单片机寄存器头文件 #include <init.h>//初始化底层驱动头文件 #include <led.h>//led,蜂鸣器,继电器底层驱动头文件 #include <key.h>//按键底层驱动头文件 #include <seg.h>//数码管底层驱动头…...
主流时序数据库深度对比:TDengine、InfluxDB与IoTDB的技术特性、性能及选型考量
目录 引言 一、 核心架构与技术特性对比 1.1、 TDengine:面向物联网的特定优化 1.2. InfluxDB:成熟的通用时序平台 1.3. Apache IoTDB:面向工业场景的精细化设计 二、 核心性能指标对比分析 2.1、写入性能 2.2、查询性能 三、 关键技…...
使用人工智能大模型腾讯元宝,如何免费快速做高质量的新闻稿?
今天我们学习使用人工智能大模型腾讯元宝,如何免费快速做高质量的新闻稿? 手把手学习视频地址:https://edu.csdn.net/learn/40402/666431 第一步在腾讯元宝对话框中输入如何协助老师做新闻稿,通过提问,我们了解了老师…...
国产Linux系统统信安装redis教程步骤
系统环境 uname -a Linux FlencherHU-PC 6.12.9-amd64-desktop-rolling #23.01.01.18 SMP PREEMPT_DYNAMIC Fri Jan 10 18:29:31 CST 2025 x86_64 GNU/Linux官网下载源码包并解压 下载链接 https://download.redis.io/releases/redis-7.0.15.tar.gz?_gl11h424d3_gcl_au*ODQ5…...
leetcode590 N叉树的后序遍历
前序遍历 的顺序是:根 → 子节点1 → 子节点2 → ... → 子节点N 后序遍历 的顺序是:子节点1 → 子节点2 → ... → 子节点N → 根 首先一个办法就是前序遍历结果进行翻转 在 迭代法 实现 后序遍历 时,如果采用 前序遍历 反转 的方式&…...
docker desktop 的安装和使用
一、Docker Desktop 是什么? Docker Desktop 是一款专为开发者设计的工具,可以在本地计算机(Windows/macOS)上快速运行和管理容器(Container)环境。以下是核心功能: 核心特点说明容器化开发基于…...
QCustomPlot频谱图
使用QCutomPlot做的读取txt文件显示频谱图的demo,帮助大家了解QCustomPlot的基本使用 1.运行结果 demo比较简单,用于文件读取,鼠标放大缩小,右键截图等基础功能. 2.绘图详解 绘图核心是将类提升为QCustomPlot之后进行重绘,重绘之前设计图表曲线,图标标题,坐标轴,坐标轴范围,背…...
Python 和 JavaScript两种语言的相似部分-由DeepSeek产生
Python 和 JavaScript 作为两种流行的编程语言,虽然在设计目标和应用场景上有差异(Python 偏向后端和脚本,JavaScript 偏向前端和动态交互),但它们的语法存在许多相似之处。以下是两者在语法上的主要共同点及对比&…...
记一次 .NET某云HIS系统 CPU爆高分析
一:背景 1. 讲故事 年前有位朋友找到我,说他们的系统会偶发性的CPU爆高,有时候是爆高几十秒,有时候高达一分多钟,自己有一点分析基础,但还是没找到原因,让我帮忙看下怎么回事? 二&…...
ESP32开发入门:基于VSCode+PlatformIO环境搭建指南
前言 ESP32作为一款功能强大的物联网开发芯片,结合PlatformIO这一现代化嵌入式开发平台,可以大幅提升开发效率。本文将详细介绍如何在VSCode中搭建ESP32开发环境,并分享实用开发技巧。 一、环境安装(Windows/macOS/Linux…...
在 macOS 上设置来电自启动
在 macOS 中,系统本身并不支持直接通过“接上电源适配器”自动开机(此功能涉及硬件底层控制)。但针对 Intel 处理器的 Mac 机型,可以通过以下方法间接实现类似效果。对于 Apple Silicon(M1/M2/M3)芯片的 Ma…...
【技术】Ruby 生态概念速查表,通过对比nodejs生态(入门)
以下是 Ruby 生态 对应概念的速查表,并使用与 Node.js 生态 对比的方式来参照,涵盖名称、作用(或解释)、简单用法、可能的替代方案,以及 Node.js 中最相似或可类比的工具(如果有的话)。有些工具…...
入门级宏基因组数据分析教程,从实验到分析与应用
宏基因组学彻底改变了研究人员对微生物群落的认识,微生物组不仅是环境组分,更作为共生体深刻影响着宿主的健康与功能。 鉴于微生物群落固有的复杂性及其所处环境的多样性,研究者进行一些宏基因组学研究时必须精心设计以获取能真实反映目标群体…...
解决 Vue 中 input 输入框被赋值后,无法再修改和编辑的问题
目录 需求: 出现 BUG: Bug 代码复现 解决问题: 解决方法1: 解决方法2 关于 $set() 的补充: 需求: 前段时间,接到了一个需求:在选择框中选中某个下拉菜单时,对应的…...
聚划算!CNN-GRU、CNN、GRU三模型多变量回归预测
聚划算!CNN-GRU、CNN、GRU三模型多变量回归预测 目录 聚划算!CNN-GRU、CNN、GRU三模型多变量回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 聚划算!CNN-GRU、CNN、GRU三模型多变量回归预测 (Matlab2023b 多输入单输出) 1.程…...
list的常见接口使用
今天,我们来讲解一下C关于STL标准库中的一个容器list的常见接口。 在我们之前c语言数据结构中,我们已经了解过了关于链表的知识点了,那么对于现在理解它也是相对来说比较容易的了。 数据结构--双向循环链表-CSDN博客 1. 定义与包含头文件 …...
5. 蓝桥公园
题目描述 小明喜欢观景,于是今天他来到了蓝桥公园。 已知公园有 N 个景点,景点和景点之间一共有 M 条道路。小明有 Q 个观景计划,每个计划包含一个起点 stst 和一个终点 eded,表示他想从 stst 去到 eded。但是小明的体力有限&am…...
零基础开始学习鸿蒙开发-智能家居APP离线版介绍
目录 1.我的小屋 2.查找设备 3.个人主页 前言 好久不发博文了,最近都忙于面试,忙于找工作,这段时间终于找到工作了。我对鸿蒙开发的激情依然没有减退,前几天做了一个鸿蒙的APP,现在给大家分享一下! 具体…...
你的 Linux 服务器连不上网?10 分钟入门网络故障排查
问题现象:服务器突然失去网络连接 当你兴冲冲地打开终端,准备开始一天的开发工作时,却发现服务器无法连接网络,ifconfig命令只能看到本地环回接口(lo)。这种突如其来的网络中断可能会让很多Linux新手感到手足无措。 别担心&…...
《Vue Router实战教程》20.路由懒加载
欢迎观看《Vue Router 实战(第4版)》视频课程 路由懒加载 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件&am…...
JVM 之 String 引用机制解析:常量池、堆内存与 intern 方法
案例一: String s1 new String("1"); String s2 "1"; System.out.println(s1 s2);s1: 执行 new String("1"),JVM 首先在字符串常量池中查找或添加字面量 "1",然后在堆内存中新建一个内容为 &quo…...
如何在 Mac 上安装 Python
所有最新的 MacOS(从 macOS 12.3 开始)都预装了 Python 版本(通常是 Python 2.x),但它已经过时并且不再受支持。要充分利用 Python 的功能,您需要安装最新版本的 Python。 本文提供了分步教程,展…...
无锡东亭无人机培训机构电话
无锡东亭无人机培训机构电话,随着科技的迅猛发展,无人机逐渐走入我们的生活和工作领域,成为多种行业中不可或缺的工具。而在其广泛的应用中,如何正确、熟练地操控无人机成为了关键。因此,找到一家专业的无人机培训机构…...
WPS复制粘贴错误 ,文件未找到 mathpage.wll
文章目录 1.错误提示图片2.解决方案1.找到MathType.wll文件和MathType Commands 2016.dotm文件并复制2.找到wps安装地址并拷贝上述两个文件到指定目录 3.重启WPS 1.错误提示图片 2.解决方案 1.找到MathType.wll文件和MathType Commands 2016.dotm文件并复制 MathType.wll地址如…...
蓝桥杯单片机刷题——按键设置当前采集距离为距离参数
设计要求 驱动超声波传感器,启动距离测量功能,并将其结果显示到数码管上。 按键“S5”定义为参数按键,按下S5按键,设备自动将当前采集的距离数据作为距离参数; 若测量的距离数据超过距离参数,指示灯L1点亮ÿ…...
mybaties plus 更新null值进入数据库
(数据库一定要支持字段为null值) 问题: 假设我现在数据库里有一个值是1,这个字段允许为null。 目前我使用的是的mybaties plus,我希望将这个值更新weinull,如何操作? 提示:如果直接…...
VSCode优雅的使用debug
原始用法:(这里不使用) 配置launch.json,里面传入参数然后debug,这里我们通常需要传入的参数比较多,而且经常修改参数,直接去修改launch.json会比较麻烦,所以使用sh脚本比较方便。 {// Use IntelliSense to…...
优化你的 REST Assured 测试:设置默认主机与端口、GET 请求与断言
REST Assured 是一个功能强大的 Java 库,用于测试 RESTful Web 服务。它简化了 API 测试流程,提供了一整套用于高效验证响应的工具。在本篇博客中,我们将深入探讨几个核心概念,包括如何设置默认主机和端口、如何发起 GET 请求以及…...
JVM之String创建、拼接
一、字符串创建的两种方式 1. 字面量直接赋值 String s1 "a"; 过程: JVM 检查字符串常量池中是否存在 "a"。若存在,直接返回常量池中的引用。若不存在,在常量池中创建 "a",返回其引用。 特点&a…...
UE5 模仿生存建造类游戏创建的过程
一、大概流程如下 点击界面按钮生成Actor->移动鼠标Actor的位置随着鼠标移动移动->点击鼠标左键确定Actor的位置 使用了盒体检测GetWorld()->SweepSingleByChannel()函数检测是否发生碰撞通过 FCollisionQueryParams CollisionParams;CollisionParams.AddIgnoredAc…...
大模型在慢性髓细胞白血病(CML)初治成人患者诊疗中的应用研究
目录 一、引言 1.1 研究背景与意义 1.2 国内外研究现状 1.3 研究目的与内容 二、大模型技术与 CML 相关知识 2.1 大模型技术原理与特点 2.2 CML 的病理生理与诊疗现状 三、术前风险预测与手术方案制定 3.1 术前数据收集与预处理 3.2 大模型预测术前风险 3.3 根据预测…...
汽车性能的幕后保障:慧通测控电动尾翼综合力学测试浅析
在汽车性能不断追求极致的当下,电动尾翼已成为众多高性能车型以及部分新能源汽车提升空气动力学表现与操控稳定性的关键配置。从炫酷的超跑到注重续航与驾驶体验的新能源车,电动尾翼正逐渐崭露头角。它绝非仅仅是外观上的装饰,而是能在车辆行…...
动力电池自动点焊机:新能源汽车制造的智能焊接利器
在新能源汽车产业蓬勃发展的今天,动力电池作为其核心部件,其性能与安全性直接关系到整车的续航里程和使用寿命。而动力电池的制造过程中,焊接工艺是至关重要的一环。这时,动力电池自动点焊机便以其高效、精准、智能的特点…...
arm64架构的copy_from_user分析
文章目录 前言代码实现内核c代码copy_from_user_copy_from_userraw_copy_from_user 内核汇编代码copy_from_user.Scopy_template.S 汇编代码分析汇编简介标签.req伪指令.macro伪指令tbz指令neg指令str指令 copy_template.S分析 小结 前言 一谈到内核-用户空间的数据拷贝&#…...
【远程工具】1.1 时间处理设计与实现(datetime库lib.rs)
一、设计原理与决策 时间单位选择 采用**秒(s)**作为基准单位,基于以下考虑: 国际单位制(SI)基本时间单位 整数秒(i64)方案优势: 精确无误差(相比浮点数&am…...
【STM32】解读启动文件startup_stm32f10x_md.s
栈空间 栈(Stack):栈是一种后进先出(LIFO)的数据结构,用于存储函数调用时的局部变量、返回地址和寄存器的值。启动文件会定义栈的大小,并将栈指针初始化为栈顶地址。在函数调用时,…...
Redis下载稳定版本5.0.4
https://www.redis.net.cn/download/ Redis下载 Redis 版本号采用标准惯例:主版本号.副版本号.补丁级别,一个副版本号就标记为一个标准发行版本,例如 1.2,2.0,2.2,2.4,2.6,2.8,奇数的副版本号用来表示非标准版本,例如2.9.x发行版本是Redis 3.0标准版本的非标准发行版本…...
阿里云服务迁移实战: 02-服务器迁移
ECS 迁移 最简单的方式是 ECS 过户,不过这里有一些限制,如果原账号是个人账号,那么目标账号无限制。如果原账号是企业账号,则指定过户给相同实名认证的企业账号。 具体操作步骤可以参考官方文档 ECS过户 进行操作。 本文重点介绍…...
怎么解决CentOS上Zookeeper启动失败的问题
在 CentOS 上启动 Zookeeper 失败通常是由于配置错误、端口冲突、权限问题或 Java 环境配置问题导致的。我们可以逐步排查: 一、查看错误日志 Zookeeper 的日志目录一般在: /your-zookeeper-path/logs/zookeeper.out 或者: /your-zookeeper-p…...
《Vue3学习手记》
下面进入Vue3的学习,以下代码中都有很详细的注释,代码也比较清晰易懂: Vue3 index.html是入口文件 Vue3通过createApp函数创建一个应用实例 main.ts: // Vue3中通过createApp函数创建应用实例 // 引入createApp用于创建应用 import { crea…...
【Ubutun】 在Linux Yocto的基础上去适配4G模块
1)、完整解决流程总结 一. 固定4G模块的网络接口名 usb0(基于物理路径) # 创建UDEV规则文件 sudo vi /etc/udev/rules.d/10-4g-rename.rules添加内容: SUBSYSTEM"net", ACTION"add", ATTRS{busnum}"2&…...
达梦数据库-学习-15-大内存SQL相关视图介绍
目录 一、环境信息 二、介绍 三、数据字典表 1、V$MEM_POOL 2、V$SQL_STAT 3、V$SQL_STAT_HISTORY 4、V$LARGE_MEM_SQLS 5、V$SYSTEM_LARGE_MEM_SQLS 四、总结 一、环境信息 名称值CPU12th Gen Intel(R) Core(TM) i7-12700H操作系统CentOS Linux release 7.9.2009 (Co…...
分治-归并系列一>翻转对
目录 题目:解析:策略一: 代码:策略二: 代码: 题目: 链接: link 这题和逆序对区别点就是,要找到前一个元素是后一个元素的2倍 先找到目标值再,继续堆排序 解析࿱…...
微服务面试题
五大组件 注册中心/配置中心 nacos 服务注册 服务启动时 将自己的id等信息发送给nacos 完成注册 服务发现 服务需要调用其他服务时 从nacos获取服务列表 交给负载均衡选择 服务监控 临时实例 由服务每隔一段时间注册中心发送信息 表示自己存活 若注册中心超过一定时间没有…...
高级java每日一道面试题-2025年3月31日-微服务篇[Nacos篇]-Nacos集群模式下的部署方案有哪些?
如果有遗漏,评论区告诉我进行补充 面试官: Nacos集群模式下的部署方案有哪些? 我回答: Nacos 集群模式下的部署方案详解 在 Java 高级面试中,Nacos 集群部署是考察候选人对分布式系统高可用性和扩展性理解的重要议题。以下是几种常见的 Nacos 集群部…...
3dmax的python通过普通的摄像头动捕表情
1、安装python 进入cdm,打python要能显示版本号 >>>(进入python提示符模式) import sys sys.path显示python的安装路径, 进入到python.exe的路径 在python目录中安装(ctrlz退出python交互模式) 2、pip install mediapipe…...