从汇编角度分析C语言的长河调用

下的汇编指令都有一些不同,或者,1.系统栈(system,该内存区用于函数的局部变量提供内存,然而每次发生函数调用,参考,2.在将数据压栈时,栈是自顶向下增长的

go的汇编觉得比较空虚,和前边接触的masm或者nasm,亦或是arm下的汇编指令都有部分例外,并且和语言本人的局部数据结构,如string或者slice相关联,要读懂那几个指令在此之前,须要先明了这么些数据结构的内部存款和储蓄器布局前些天由此对一小段汇编指令的剖判,来越来越深造go的汇编

➠更加的多才能干货请戳:听云博客

函数调用是一个最轻松易行不过的概念了,然则每一次发生函数调用,CPU
和操作系统内核都做了汪洋的干活。那篇文章只剖判到 compute() 函数实施。
参考

➠更加的多才能干货请戳:听云博客

先上源代码
type Bean struct { Name string}func main() { m := make(map[string]*Bean) b := Bean{"Jim"} m["Jim"] = &b fmt.Println}

简书援救代码高亮了呢?

本次只对最终一条语句的汇编举行解析使用

$ go tool compile -S main.go >> main.S

扭转汇编文件 main.S打开以往,找到该语句所对应的汇编指令

亟待先介绍一下map的取值函数签字func mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer以此函数,该函数位于runtime包中

 ;获取*maptype LEAQ type.map[string]*"".Bean, AX AX,  ;获取*hmap,map实际上是一个*hmap类型指针 LEAQ ""..autotmp_6+72, AX MOVQ AX, 8 ;构造参数key,key是一个string,由字节数组和len字段构成 ;字符串的字节数组 LEAQ go.string."Jim", AX MOVQ AX, 16 ;字符串的字符长度3 MOVQ $3, 24 PCDATA $0, $1 ;通过key获取map中的value CALL runtime.mapaccess1_faststr ;获取mapaccess1_faststr的返回值,是unsafe.Pointer(&value) MOVQ 32, AX ;获取value的内容,因为value类型是指针,直接保存到AX MOVQ , AX ;内存置零 XORPS X0, X0 MOVUPS X0, ""..autotmp_4+56 ;构造*Bean的空接口值 ;*Bean的type LEAQ type.*"".Bean, CX MOVQ CX, ""..autotmp_4+56 ;AX保存的是*Bean的类型 MOVQ AX, ""..autotmp_4+64 ;保存空接口值的地址到AX LEAQ ""..autotmp_4+56, AX ;可变长参数实际上是一个slice,底层数组只有一个元素 MOVQ AX,  ;len为1 MOVQ $1, 8 ;cap为1 MOVQ $1, 16 PCDATA $0, $3 ;调用fmt.Println CALL fmt.Println

汇编高亮?

go的汇编中,调用二个函数时,调用方要先把参数类型的内部存款和储蓄器布局和函数的参数评释顺序放到SP伪贮存器对应的函数栈中,而被调用函数把重回值写到紧接参数之后的岗位,调用方能够直接获取到

主导术语定义

caller 和 callee

率先说雅培个概念,下边包车型客车代码片段中,大家说 main
函数是主调函数(caller),compute 函数则是被调函数(callee)。

int main(void)
{
    compute(2, 4); 
    return 0;
}    

主干术语定义

1.系统栈(system
stack)是二个内部存款和储蓄器区,位于进度地址空间的末端。

准备

// compute.c
// 加法操作
int add(int c, int d)
{
    int e = c + d;
    return e;
}

// 运算
int compute(int a, int b)
{
    int e = add(a, b);
    return e;
}

// 主函数
int main(void)
{
    compute(2, 4);
    return 0;
}         

将以上这么些 C
程序编写翻译获得可试行文件,当以此可试行文件运维时,在操作系统中对应叁个进度,这么些进度在客户态对应二个调用栈结构(call
stack)。程序中每一个未到位运营的函数对应多少个栈帧(stack
frame),栈帧能够精晓成贰个函数在栈上对应的一段连接空间。栈帧中保存函数片段变量、传递给被调函数的参数等音信。
当前帧的范围是由八个寄放器来界定的。那五个存放器分别位 BP(Base
Pointer)
寄放器和 SP(Stack Pointer) 贮存器。BP
存放器也叫基址存放器。SP
贮存器也叫栈顶寄放器。另外栈底对应高地址,栈顶对应低地址,栈由内部存储器高地址向低地址生长。上面所说的是一些概念,先明了这一个概念,然后大约知道程序实践过程中,随着函数调用的发生,该进度对应的栈结构大意上如下。

图片 1

1.系统栈(system
stack)是三个内部存款和储蓄器区,位于进度地址空间的前面。

2.在将数据压栈时,栈是自顶向下坚实的,该内部存款和储蓄器区用于函数的局部变量提供内部存储器。它也辅助在调用函数时传递参数。

寄存器

贮存器位于CPU内部,用于贮存程序施行中用到的多少和指令,CPU从寄存器中取数据,相比较从内部存款和储蓄器中取快得多。寄放器又分通用存放器特别规寄放器
通用存放器有
ax/bx/cx/dx/di/si,固然这几个存放器在大繁多下令中能够轻巧选择,但也是有局地非正规的鲜明,比如一些指令只好用有些特定的通用贮存器,比如函数重临时,需将重临值
mov 到 ax 贮存器中,特殊存放器有 bp/sp/ip
等,特殊贮存器均有特定用途,对于有一定用途的多少个寄放器,简介如下:
ax(accumulator): 可用于存放函数再次回到值
bp(base pointer): 用于寄放实行中的函数对应的栈帧的栈底地址
sp(stack poinger): 用于贮存推行中的函数对应的栈帧的栈顶地址
ip(instruction pointer): 指向当前举行命令的下一条指令
另外不一样架构的 CPU,存放器名称被添以差异前缀以提示贮存器的轻重。举个例子对于
x86 架构,字母 “e” 用作名称前缀,提示各贮存器大小为 32 位,对于 x86_64
贮存器,字母 “r” 用作名称前缀,提醒各存放器大小为 64 位。

2.在将数据压栈时,栈是自顶向下加强的,该内部存款和储蓄器区用于函数的一对变量提供内部存款和储蓄器。它也支撑在调用函数时传递参数。

3.假诺调用了嵌套的长河,栈会自上而下增进,并接受新的移动记录(activation
record)来保存三个进度所需的富有数据。

编译

率先我们编写翻译下面的前后相继,假使想用 gdb 调节和测验工具进行调整,这里不可不抬高 -g
参数,加上 -g 参数后,目的文件中能力饱含调节和测量检验要用到的音信。

gcc -g compute.c -o compute

3.倘若调用了嵌套的历程,栈会自上而下拉长,并接受新的移位记录(activation
record)来保存三个历程所需的持有数据。

4.当下实施进度的移位记录,由标识顶端地点的帧指针(frame
point)和符号头部地点的栈指针(stack point)定义。

反汇编深入分析

输入以下命令,进入调节和测量检验情状。

gdb compute

进去调节和测量检验遭遇以往,输入start命令开端调节和测验。start
命令用于拉起被调节和测验程序,并施行至 main
函数的开首地方,程序被施行之后与八个客商态的调用栈关联。

start

首要的出口新闻如下:

Temporary breakpoint 1, main () at compute.c:26
26      compute(2, 4);

这段日子我们的次第跑在main函数中,并在第 26 行处,也正是 compute(2, 4)
那一个职责停住了(该行的代码还没推行)。大家disassemble命令展现当前函数的汇编音讯。我们用到了-r参数和-m参数。-m参数是点名显示的Computer指令用16进制表示。/m参数钦点突显汇编指令的还要,展现相应的源代码。

disassemble /rm

展现的要紧结果如下,当中 # 前面包车型地铁内容为人工加多注释。

Dump of assembler code for function main:
25  {  # 源文件行号,该行代码
   0x000000000040055b <+0>: 55  push   %rbp
   0x000000000040055c <+1>: 48 89 e5    mov    %rsp,%rbp

26      compute(2, 4);
# 注意下面这个箭头,表明程序现在停在这个地方,该行代码还没有执行。
=> 0x000000000040055f <+4>: be 04 00 00 00  mov    $0x4,%esi
   0x0000000000400564 <+9>: bf 02 00 00 00  mov    $0x2,%edi
   0x0000000000400569 <+14>:    e8 b2 ff ff ff  callq  0x400520 <compute>

27      return 0;
   0x000000000040056e <+19>:    b8 00 00 00 00  mov    $0x0,%eax

28  }
   0x0000000000400573 <+24>:    5d  pop    %rbp
   0x0000000000400574 <+25>:    c3  retq   

End of assembler dump.

对此地点的输出,介绍一下0x000000000040055b <+0>: 55 push %rbp逐个字段的含义。

- 0x000000000040055b: 该指令对应的虚拟内存地址
<+0>: 该指令的虚拟内存地址偏移量
55: 该指令对应的计算机指令
push %rbp: 汇编指令

骨子里 main 函数并非前后相继并非前后相继拉起后的第三个实践的函数,main
函数也是贰个被调函数。它被 _start 函数调用,这里不追究。只需求了然 main
函数是也被叁个叫 _start 的函数调用的就能够。这里也先不分析上边两行。

push   %rbp
mov    %rsp,%rbp

_start
函数施行时,栈上意况大致如下图所示,大家用银色代表正在实行的函数的栈帧:

图片 2

当 _start 函数调用了此地的 main
函数后,分公司方给出的出口结果中=>的岗位来看,那时的 main
函数刚刚起头实行,栈上的气象大概如下图所示:

图片 3

进行以下命令,执行 3 行汇编代码。

si 3

由于实行完 start 命令后,程序停在 0x000000000040055c
位置,所以这里进行以下三行代码

mov    $0x4,%esi
mov    $0x2,%edi
callq  0x400520 <compute>

八个函数调用另贰个函数,需先将参数希图好。main 函数调用 compute
函数,所以前两行代码正是将三个参数字传送入通用存放器中,对于参数字传送递的点子,x86和x86_64概念了区别的函数调用规约(calling
convention)。相比x86_64将参数字传送入通用寄放器的法子,x86则是将参数压入调用栈中。那又是另四个专项论题了,不做探究,接下去就要实践call 指令了。

callq  0x400520 <compute>

这是一条 call 指令,call 指令要成功四个职分。

  1. 将主调函数 main 中的下一条指令(callq
    的下一条指令)所在的虚构内部存款和储蓄器地址压入栈中(这里为 0x000000000040056e
    )压入栈中,被调函数(compute)重回后将取那几个地点的一声令下继续执行。随地随时要注意,每一趟入栈操作,rsp
    贮存器的值都是会更新的。
  2. call 指令会更新 rip
    寄放器的值,使其值为被调函数(compute)所在的苗子地址,这里为
    0x400520。

当 call 指令实行到位后,那个时候,程序就进行到 compute
函数里了。我们依然采取以下命令查看当前函数的汇编信息。

disassemble /rm

浮现的结果如下:

Dump of assembler code for function compute:
18  {
=> 0x0000000000400520 <+0>: 55  push   %rbp
   0x0000000000400521 <+1>: 48 89 e5    mov    %rsp,%rbp
   0x0000000000400524 <+4>: 48 83 ec 18 sub    $0x18,%rsp
   0x0000000000400528 <+8>: 89 7d ec    mov    %edi,-0x14(%rbp)
   0x000000000040052b <+11>:    89 75 e8    mov    %esi,-0x18(%rbp)

19      int e = add(a, b);
   0x000000000040052e <+14>:    8b 55 e8    mov    -0x18(%rbp),%edx
   0x0000000000400531 <+17>:    8b 45 ec    mov    -0x14(%rbp),%eax
   0x0000000000400534 <+20>:    89 d6   mov    %edx,%esi
   0x0000000000400536 <+22>:    89 c7   mov    %eax,%edi
   0x0000000000400538 <+24>:    e8 b0 ff ff ff  callq  0x4004ed <add>
   0x000000000040053d <+29>:    89 45 f8    mov    %eax,-0x8(%rbp)

20      int f = mul(a, b);
   0x0000000000400540 <+32>:    8b 55 e8    mov    -0x18(%rbp),%edx
   0x0000000000400543 <+35>:    8b 45 ec    mov    -0x14(%rbp),%eax
   0x0000000000400546 <+38>:    89 d6   mov    %edx,%esi
   0x0000000000400548 <+40>:    89 c7   mov    %eax,%edi
   0x000000000040054a <+42>:    e8 b8 ff ff ff  callq  0x400507 <mul>
   0x000000000040054f <+47>:    89 45 fc    mov    %eax,-0x4(%rbp)

21      return e * f;
   0x0000000000400552 <+50>:    8b 45 f8    mov    -0x8(%rbp),%eax
   0x0000000000400555 <+53>:    0f af 45 fc imul   -0x4(%rbp),%eax

22  }
   0x0000000000400559 <+57>:    c9  leaveq 
   0x000000000040055a <+58>:    c3  retq   

End of assembler dump.

实施以下命令,将 rbp 寄放器中的地址入栈,然后将 rsp 中的地址 赋值给 rbp
存放器(也正是让 rbp 指向当前 rsp)。

si 2

那时候栈上的意况如下图所示:

图片 4

接下去要施行的话语正是上边这条语句。那条语句的含义是栈帧扩张。大家上文提到过,栈从高地址向第地址生长,所以减操作是栈的恢宏操作。这里正是为被调用的函数的栈帧预先开荒空间,空间大小为22个字节。到那边就分析到位了,接下去就是compute()
函数的实践进度了。个人力量简单,某个地点实行说不清楚,大家能够自动更加深远的钻研一下。

sub    $0x18,%rsp

这里补充一个定义,计算机是按字节编址,按字节编址的意思就是每一个地点对应叁个字节。63个人操作系统,各个地方由 8 个字节表示。

4.当下奉行进度的运动记录,由标志顶上部分地点的帧指针(frame
point)和标识尾部地方的栈指针(stack point)定义。

5.在进程举行时,固然其顶上部分的限定是稳固的,但尾部的范围是足以扩展的(在须要越多内部存款和储蓄器空间时)。

5.在进度进行时,即便其最上部的范围是永远的,但尾巴部分的界定是足以增添的(在需求越多内部存款和储蓄器空间时)。

分析栈帧(深入分析如下)

浅析栈帧

图片 5

图片 5

上海体育地方第4个栈帧的剖判如下: 

上海教室第二个栈帧的分析如下:

1、在栈帧顶端是重回地址,以及保存的旧的帧指针。再次来到地址钦定了现阶段进程甘休时期码的调整流转向的内部存款和储蓄器地址,而保留的旧的帧指针则是前二个平移记录的帧指针。在当下历程停止后,该帧指针的值可用以重新建立调用进度的栈帧,在试图调节和测量试验调用栈回溯时,那一点很主要。

1、在栈帧顶端是再次来到地址,以及保存的旧的帧指针。重回地址钦赐了现阶段进度截止时期码的调控流转向的内部存款和储蓄器地址,而保留的旧的帧指针则是前三个平移记录的帧指针。在现阶段历程停止后,该帧指针的值可用以重新建立调用进度的栈帧,在计算调节和测量试验调用栈回溯时,那一点相当重大。

2、活动记录的要紧部分是为经过调用局地变量分配的内部存款和储蓄器空间。在C中,这种变量也叫做自动变量(automatic
variable)。

2、活动记录的机要部分是为经过调用局地变量分配的内部存款和储蓄器空间。在C中,这种变量也堪称自动变量(automatic
variable)。

3、在函数调用时,以参数情势传递到函数的值,存储在栈的平底。

3、在函数调用时,以参数情势传递到函数的值,存储在栈的最底层。

4、全部大面积的Computer类别布局都提供了以下多个栈操作指令:

4、全数大面积的管理器系列布局都提供了以下七个栈操作指令:

  • push指令将一个值放置在栈上,并将栈指针esp减去该值所占领的内存字节数。栈的前面下移到更低的地址;

  • pop指令从栈中弹出二个值,并相应增添栈指针esp的值,也便是说,栈的前边上移了。

  • push指令将四个值放置在栈上,并将栈指针esp减去该值所占有的内部存款和储蓄器字节数。栈的末尾下移到更低的地点;

  • pop指令从栈中弹出三个值,并相应扩大栈指针esp的值,也正是说,栈的末尾上移了。

5、平日类别布局另外提供三个指令,用于调用和退出函数(自动重临到调用进度),它们也会自动操作栈:

5、日常连串布局另外提供四个指令,用于调用和退出函数(自动重返到调用进度),它们也会自动操作栈:

  • call指令将指令指针的前段时间值压栈,跳转到被调用函数的前奏地址。
     call 指令 :在AT&T汇编中,call
    foo(foo是二个表明)等效于以下汇编指令: pushl %eip ,movl f, %eip
     ;   

  • return指令从栈上弹出返回地址,并跳转到该地址。进度的贯彻必得将rerurn作为最后一条指令,由call放置在栈上的归来地址位于栈的尾巴部分(实际上是上二个运动记录的底层,当前运动记录的顶端)。
     ret指令: 在AT&T汇编中,ret等效于以下汇编指令: popl %eip

  • call指令将指令指针的当前值压栈,跳转到被调用函数的开局部址。
    call 指令 :在AT&T汇编中,call foo等效于以下汇编指令: pushl %eip
    ,movl f, %eip ;

  • return指令从栈上弹出重临地址,并跳转到该地址。进程的达成必需将rerurn作为最后一条指令,由call放置在栈上的归来地址位于栈的最底层(实际上是上八个平移记录的平底,当前运动记录的顶上部分)。
    ret指令: 在AT&T汇编中,ret等效于以下汇编指令: popl %eip