这一组数组的初值是zero-金沙js娱乐场官方网站valued,即把原array内容做一个拷贝

而不是slice所包含的数据的值,即caller中记录的len值,这篇文章以实践的方式验证go语言函数之间是如何传递数组类型变量的,即把原array内容做一个拷贝,Go中的函数可以有多个返回值,也可以没有参数,在go语言中数组array是一组特定长度的有序的元素集合,    //这里我们创建了一个长度为5的数组.,本文将详解C/C++堆栈的工作机制,什么时候数据存储在堆栈(Stack)中

在上述例子中

package mainconst SIZE = 16func main() { var ss [SIZE]int64 ss[0] = 0x1111 ss[SIZE-1] = 0x2222 useArray}func useArray(ss [SIZE]int64) { ss[0] = 0x3333 ss[SIZE-1] = 0x4444}

Go中函数特性简介

对Go中的函数特性做一个总结。懂则看,不懂则算。

  1. Go中有3种函数:普通函数、匿名函数、方法(定义在struct上的函数)。
  2. Go编译时不在乎函数的定义位置,但建议init()定义在最前面,main函数定义在init()之后,然后再根据函数名的字母顺序或者根据调用顺序放置各函数的位置。
  3. 函数的参数、返回值以及它们的类型,结合起来成为函数的签名(signature)。
  4. 函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。
    • 由于引用类型(slice、map、interface、channel)自身就是指针,所以这些类型的值拷贝给函数参数,函数内部的参数仍然指向它们的底层数据结构。
  5. 函数参数可以没有名称,例如func myfunc
  6. Go中的函数可以作为一种type类型,例如type myfunc func int
    • 实际上,在Go中,函数本身就是一种类型,它的signature就是所谓的type,例如func int。所以,当函数ab()赋值给一个变量ref_abref_ab := ab,不能再将其它函数类型的函数cd()赋值给变量ref_ab
  7. Go中作用域是词法作用域,意味着函数的定义位置决定了它能看见的变量。
  8. Go中不允许函数重载,也就是说不允许函数同名。
  9. Go中的函数不能嵌套函数,但可以嵌套匿名函数。
  10. Go实现了一级函数(first-class
    functions),Go中的函数是高阶函数(high-order functions)。这意味着:

    • 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数
    • 函数可以作为参数传递给另一个函数
    • 函数的返回值可以是一个函数
    • 这些特性使得函数变得无比的灵活,例如回调函数、闭包等等功能都依赖于这些特性。
  11. Go中的函数不支持泛型,但如果需要泛型的情况,大多数时候都可以通过接口、type
    switch、reflection的方式来解决。但使用这些技术使得代码变得更复杂,性能更低。

func main() {

   EBP指针入栈

   
在foo函数中,首先将EBP寄存器的值压入堆栈。因为此时EBP寄存器的值还是用于main函数的,用来访问main函数的参数和局部变量的,因此需要将它暂存在堆栈中,在foo函数退出时恢复。同时,给EBP赋于新值。

    1)将EBP压入堆栈

    2)把ESP的值赋给EBP

图4

   
这样一来,我们很容易发现当前EBP寄存器指向的堆栈地址就是EBP先前值的地址,你还会发现发现,EBP+4的地址就是函数返回值的地址,EBP+8就是函数的第一个参数的地址(第一个参数地址并不一定是EBP+8,后文中将讲到)。因此,通过EBP很容易查找函数是被谁调用的或者访问函数的参数(或局部变量)。 

 var ss []int64 467f0d: 48 c7 44 24 18 00 00 movq $0x0,0x18 467f14: 00 00 467f16: 48 c7 44 24 20 00 00 movq $0x0,0x20 467f1d: 00 00 467f1f: 48 c7 44 24 28 00 00 movq $0x0,0x28 467f26: 00 00 useSlice 467f28: 48 c7 04 24 00 00 00 movq $0x0, # data pointer 467f2f: 00 467f30: 48 c7 44 24 08 00 00 movq $0x0,0x8 # len value 467f37: 00 00 467f39: 48 c7 44 24 10 00 00 movq $0x0,0x10 # cap value 467f40: 00 00 467f42: e8 19 00 00 00 callq 467f60 <main.useSlice>

以如下go语言程序为例子:

匿名函数

匿名函数是没有名称的函数。一般匿名函数嵌套在函数内部,或者赋值给一个变量,或者作为一个表达式。

定义的方式:

// 声明匿名函数func{    ...CODE...}// 声明匿名函数并直接执行func{    ...CODE...}(parameters)

下面的示例中,先定义了匿名函数,将其赋值给了一个变量,然后在需要的地方再去调用执行它。

package mainimport "fmt"func main() {    // 匿名函数赋值给变量    a := func() {        fmt.Println("hello world")    }    // 调用匿名函数    a()    fmt.Printf("%T\n", a) // a的type类型:func()    fmt.Println        // 函数的地址}

如果给匿名函数的定义语句后面加上(),表示声明这个匿名函数的同时并执行:

func main() {    msg := "Hello World"    func {        fmt.Println    }}

其中func表示匿名函数的参数,func{}msg表示传递msg变量给匿名函数,并执行。

    //通过这个语法声明数组的默认初值
    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println(“dcl:”, b)

金沙js娱乐场官方网站 1从一些基本的知识和概念开始

    1) 程序的堆栈是由处理器直接支持的。在intel
x86的系统中,堆栈在内存中是从高地址向低地址扩展(这和自定义的堆栈从低地址向高地址扩展不同),如下图所示:

金沙js娱乐场官方网站 2开始讨论堆栈是如何工作的

   
我们来讨论堆栈的工作机制。堆栈是用来支持函数的调用和执行的,因此,我们下面将通过一组函数调用的例子来讲解,看下面的代码:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int foo1(int m, int n) {     int p=m*n;     return p; } int foo(int a, int b) {     int c=a+1;     int d=b+1;     int e=foo1(c,d);     return e; }   int main() {     int result=foo(3,4);     return 0; }

   
这段代码本身并没有实际的意义,我们只是用它来跟踪堆栈。下面的章节我们来跟踪堆栈的建立,堆栈的使用和堆栈的销毁。

$ go build && ./mains1 p=0xc42000a2a0,v1=c42000a2c0,v2=2,v=4s2 p=0xc42000a320,v1=c42000a2c0,v2=2,v=4s2 p=0xc42000a320,v1=c42000a2c0,v2=3,v=4s1 p=0xc42000a2a0,v1=c42000a2c0,v2=2,v=4s3 p=0xc42000a300,v1=c42000a2c0,v2=3,v=4
 44d697: e8 ee ab ff ff callq 44828a <runtime.duffzero+0x10a>... 44d6cd: e8 fe ae ff ff callq 4485d0 <runtime.duffcopy+0x310>

func type

可以将func作为一种type,以后可以直接使用这个type来定义函数。

package mainimport "fmt"type add func intfunc main() {    var a add = func int{        return a+b    }    s := a    fmt.Println}

Arrays:数组

金沙js娱乐场官方网站 3堆栈帧的销毁

   
当函数将返回值赋予某些寄存器或者拷贝到堆栈的某个地方后,函数开始清理堆栈帧,准备退出。堆栈帧的清理顺序和堆栈建立的顺序刚好相反:(堆栈帧的销毁过程就不一一画图说明了)

   1)如果有对象存储在堆栈帧中,对象的析构函数会被函数调用。

    2)从堆栈中弹出先前的通用寄存器的值,恢复通用寄存器。

   
3)ESP加上某个值,回收局部变量的地址空间(加上的值和堆栈帧建立时分配给局部变量的地址大小相同)。

    4)从堆栈中弹出先前的EBP寄存器的值,恢复EBP寄存器。

    5)从堆栈中弹出函数的返回地址,准备跳转到函数的返回地址处继续执行。

    6)ESP加上某个值,回收所有的参数地址。

   
前面1-5条都是由callee完成的。而第6条,参数地址的回收,是由caller或者callee完成是由函数使用的调用约定(calling
convention )来决定的。下面的小节我们就来讲解函数的调用约定。

func useSlice(s2 []int64) []int64 { append return s2}

从这段代码可以看到,main函数把ss的内容做了一个完整拷贝,函数runtime.duffcopy用来拷贝内存从%rsi拷贝到%rdi,细心的读者会发现这个函数和前面的runtime.duffzero函数一样有一个问题,即没有指定内存的大小,不知道该拷贝填充多大的内存,虽然指定了内存地址的开始地址,但是没有指定结束地址。这其实就是这两个函数设计的巧妙之处,后面我们再介绍。

内置函数

在builtin包中有一些内置函数,这些内置函数额外的导入包就能使用。

有以下内置函数:

$ go doc builtin | grep funcfunc close(c chan<- Type)func delete(m map[Type]Type1, key Type)func panic(v interface{})func print(args ...Type)func println(args ...Type)func recover() interface{}    func complex(r, i FloatType) ComplexType    func imag(c ComplexType) FloatType    func real(c ComplexType) FloatType    func append(slice []Type, elems ...Type) []Type    func make(t Type, size ...IntegerType) Type    func new *Type    func cap int    func copy(dst, src []Type) int    func len int
  • close用于关闭channel
  • delete用于删除map中的元素
  • copy用于拷贝slice
  • append用于追加slice
  • cap用于获取slice的容量
  • len用于获取
    • slice的长度
    • map的元素个数
    • array的元素个数
    • 指向array的指针时,获取array的长度
    • string的字节数
    • channel的channel buffer中的未读队列长度
  • printprintln:底层的输出函数,用来调试用。在实际程序中,应该使用fmt中的print类函数
  • compleximagreal:操作复数
  • panicrecover:处理错误
  • newmake:分配内存并初始化
    • new适用于为值类(value
      type)的数据类型(如array,int等)和struct类型的对象分配内存并初始化,并返回它们的指针给变量。如v := new
    • make适用于为内置的引用类的类型(如slice、map、channel等)分配内存并初始化底层数据结构,并返回它们的指针给变量,同时可能会做一些额外的操作

注意,地址和指针是不同的。地址就是数据对象在内存中的地址,指针则是占用一个机器字长(32位机器是4字节,64位机器是8字节)的数据,这个数据中存储的是它所指向数据对象的地址。

a -> AAAAb -> Pointer -> BBBB

new构造数据对象赋值给变量的都是指向数据对象的指针。

package main

金沙js娱乐场官方网站 4返回值是如何传递的

   
堆栈帧建立起后,函数的代码真正地开始执行,它会操作堆栈中的参数,操作堆栈中的局部变量,甚至在堆(Heap)上创建对象,balabala….,终于函数完成了它的工作,有些函数需要将结果返回给它的上一层函数,这是怎么做的呢?

   
首先,caller和callee在这个问题上要有一个“约定”,由于caller是不知道callee内部是如何执行的,因此caller需要从callee的函数声明就可以知道应该从什么地方取得返回值。同样的,callee不能随便把返回值放在某个寄存器或者内存中而指望Caller能够正确地获得的,它应该根据函数的声明,按照“约定”把返回值放在正确的”地方“。下面我们来讲解这个“约定”:  
   
1)首先,如果返回值等于4字节,函数将把返回值赋予EAX寄存器,通过EAX寄存器返回。例如返回值是字节、字、双字、布尔型、指针等类型,都通过EAX寄存器返回。

   
2)如果返回值等于8字节,函数将把返回值赋予EAX和EDX寄存器,通过EAX和EDX寄存器返回,EDX存储高位4字节,EAX存储低位4字节。例如返回值类型为__int64或者8字节的结构体通过EAX和EDX返回。

    3) 
如果返回值为double或float型,函数将把返回值赋予浮点寄存器,通过浮点寄存器返回。

   
4)如果返回值是一个大于8字节的数据,将如何传递返回值呢?这是一个比较麻烦的问题,我们将详细讲解:

        我们修改foo函数的定义如下并将它的代码做适当的修改:

1 2 3 4 MyStruct foo(int a, int b) { ... }

         MyStruct定义为:

1 2 3 4 5 6 struct MyStruct {     int value1;     __int64 value2;     bool value3; };

     这时,在调用foo函数时参数的入栈过程会有所不同,如下图所示:

图10

   
caller会在压入最左边的参数后,再压入一个指针,我们姑且叫它ReturnValuePointer,ReturnValuePointer指向caller局部变量区的一块未命名的地址,这块地址将用来存储callee的返回值。函数返回时,callee把返回值拷贝到ReturnValuePointer指向的地址中,然后把ReturnValuePointer的地址赋予EAX寄存器。函数返回后,caller通过EAX寄存器找到ReturnValuePointer,然后通过ReturnValuePointer找到返回值,最后,caller把返回值拷贝到负责接收的局部变量上(如果接收返回值的话)。

   
你或许会有这样的疑问,函数返回后,对应的堆栈帧已经被销毁,而ReturnValuePointer是在该堆栈帧中,不也应该被销毁了吗?对的,堆栈帧是被销毁了,但是程序不会自动清理其中的值,因此ReturnValuePointer中的值还是有效的。

./main.go:<line>: append evaluated but not used

也就是直接调用函数的入口指令,而这两个函数都是带一个偏移量的,这也就只有像runtime.duffzero和runtime.duffcopy这种内部逻辑简单的函数可以这么调用。这实际上编译器处理了大量的工作。

按值传参

Go中是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。

例如:

a,b := 10,20minfunc min int{}

上面调用min()时,是将a和b的值拷贝一份,然后将拷贝的副本赋值给变量x,y的,所以min()函数内部,访问、修改的一直是a、b的副本,和原始的数据对象a、b没有任何关系。

如果想要修改外部数据,需要传递指针。

例如,下面两个函数,func_value()是传值函数,func_ptr()是传指针函数,它们都修改同一个变量的值。

package mainimport "fmt"func main() {    a := 10    func_value    fmt.Println    // 输出的值仍然是10        b := &a    func_ptr    fmt.Println   // 输出修改后的值:11}func func_value int{    x = x + 1    return x}func func_ptr int{    *x = *x + 1    return *x}

map、slice、interface、channel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。

另外注意,赋值操作b = a+1这种类型的赋值也是拷贝赋值。换句话说,现在底层已经有两个数据对象,一个是a,一个是b。但a = a+1这种类型的赋值虽然本质上是拷贝赋值,但因为a的指针指向特性,使得结果上看是原地修改数据对象而非生成新数据对象。

    //获取某个键的值
    v1 := m[“k1”]
    fmt.Println(“v1: “, v1)

C/C++堆栈指引(转),堆栈指引

有同学可能会疑问了,append既然输出参数就是出入参数,那不是多此一举吗,不用处理返回也行啊:

看前面的代码,通常的函数调用都是

递归函数

函数内部调用函数自身的函数称为递归函数。

使用递归函数最重要的三点:

  1. 必须先定义函数的退出条件,退出条件基本上都使用退出点来定义,退出点常常也称为递归的基点,是递归函数的最后一次递归点,或者说没有东西可递归时就是退出点。
  2. 递归函数很可能会产生一大堆的goroutine(其它编程语言则是出现一大堆的线程、进程),也很可能会出现栈空间内存溢出问题。在其它编程语言可能只能设置最大递归深度或改写递归函数来解决这个问题,在Go中可以使用channel+goroutine设计的”lazy
    evaluation”来解决。
  3. 递归函数通常可以使用level级数的方式进行改写,使其不再是递归函数,这样就不会有第2点的问题。

例如,递归最常见的示例,求一个给定整数的阶乘。因为阶乘的公式为n**...*3*2*1,它在参数为1的时候退出函数,也就是说它的递归基点是1,所以对是否为基点进行判断,然后再写递归表达式。

package mainimport "fmt"func main() {    fmt.Println}func a int{    // 判断退出点    if n == 1 {        return 1    }    // 递归表达式    return n * a}

它的调用过程大概是这样的:

金沙js娱乐场官方网站 5

再比如斐波那契数列,它的计算公式为f+ff=1。它在参数为1和2的时候退出函数,所以它的退出点为1和2。

package mainimport "fmt"func main() {    fmt.Println}func f int{    // 退出点判断    if n == 1 || n == 2 {        return 1    }    // 递归表达式    return f+f}

如何递归一个目录?它的递归基点是文件,只要是文件就返回,只要是目录就进入。所以,伪代码如下:

func recur FILE{    // 退出点判断    if (dir is a file){        return dir    }    // 当前目录的文件列表    file_slice := filelist()        // 遍历所有文件    for _,file := range file_slice {        return recur    }}

import “fmt”

金沙js娱乐场官方网站 6参考

Debug Tutorial Part 2: The Stack

Intel汇编语言程序设计(第四版) 第8章

运行结果如下:

因为我们看两条赋值语句的代码:ss[0]对应的是%rsp +
0x80ss[15]对应的是%rsp + 0xf8

参数和返回值

函数可以有0或多个参数,0或多个返回值,参数和返回值都需要指定数据类型,返回值通过return关键字来指定。

return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。Go中的函数可以有多个返回值。

  • .当返回值有多个时,这些返回值必须使用括号包围,逗号分隔
  • .return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称
  • .当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值
  • .但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高
  • .命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义
  • .return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b是正确的,但return c=a+b是错误的

例如:

// 单个返回值func func_a() int{    return a}// 只要命名了返回值,必须括号包围func func_b{    // 变量a int已存在,无需再次声明    a = 10    return    // 等价于:return a}// 多个返回值,且在return中指定返回的内容func func_c() {    return a,b}// 多个返回值func func_d() {    return    // 等价于:return a,b}// return覆盖命名返回值func func_e() {    return x,y}

Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如return value,existsreturn value,okreturn value,err等。

当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。

但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线_这个blank
identifier来丢弃这些返回值。例如下面的func_a函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量a

func func_a() {    return}func main() {    a,_ := func_a()}

复制代码 代码如下:

金沙js娱乐场官方网站 7前言

   
我们经常会讨论这样的问题:什么时候数据存储在堆栈(Stack)中,什么时候数据存储在堆(Heap)中。我们知道,局部变量是存储在堆栈中的;debug时,查看堆栈可以知道函数的调用顺序;函数调用时传递参数,事实上是把参数压入堆栈,听起来,堆栈象一个大杂烩。那么,堆栈(Stack)到底是如何工作的呢?
本文将详解C/C++堆栈的工作机制。阅读时请注意以下几点:

    1)本文讨论的编译环境是 Visual
C/C++,由于高级语言的堆栈工作机制大致相同,因此对其他编译环境或高级语言如C#也有意义。

   
2)本文讨论的堆栈,是指程序为每个线程分配的默认堆栈,用以支持程序的运行,而不是指程序员为了实现算法而自己定义的堆栈。

    3)  本文讨论的平台为intel x86。

   
4)本文的主要部分将尽量避免涉及到汇编的知识,在本文最后可选章节,给出前面章节的反编译代码和注释。

   
5)结构化异常处理也是通过堆栈来实现的(当你使用try…catch语句时,使用的就是c++对windows结构化异常处理的扩展),但是关于结构化异常处理的主题太复杂了,本文将不会涉及到。

  1. 第一个是s1和第二个s1输出的值一模一样,这是在调用useSlice前后打出来的,可见尽管在useSlice里面修改的slice的值,但是main函数并不知道。
  2. 所有输出的v1值都是相同的,即他们指向的数据存储地址是同一块地址。
  3. s2的两次输出,除了v2值加一以为,其他都是一样的,说明此时append函数的返回值,就是append传入参数的值。
  4. s3的值是新分配的slice对象,它里面的值和第二个s2输出时一样的,即是useSlice函数的返回值。

接着再看函数useArray的实现语句

变长参数”…”

有时候参数过多,或者想要让函数处理任意多个的参数,可以在函数定义语句的参数部分使用ARGS...TYPE的方式。这时会将...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。

...在Go中称为variadic,在使用...的时候,可以将它看作是一个slice,下面的几个例子可以说明它的用法。

例如:func myfunc(a,b int,args...int) int {}。除了前两个参数a和b外,其它的参数全都保存到名为args的slice中,且这些参数全都是int类型。所以,在函数内部就已经有了一个args = []int{....}的数据结构。

例如,下面的例子中,min()函数要从所有参数中找出最小的值。为了实验效果,特地将前两个参数a和b独立到slice的外面。min()函数内部同时会输出保存到args中的参数值。

package mainimport "fmt"func main() {    a,b,c,d,e,f := 10,20,30,40,50,60    fmt.Println(min(a,b,c,d,e,f))}func min(a,b int,args...int) int{    // 输出args中保存的参数    // 等价于 args := []int{30,40,50,60}    for index,value := range args {        fmt.Printf("%s%d%s %d\n","args[",index,"]:",value)    }    // 取出a、b中较小者    min_value := a    if a>b {        min_value = b    }    // 取出所有参数中最小值    for _,value := range args{        if min_value > value {            min_value = value        }    }    return min_value}

但上面代码中调用函数时传递参数的方式显然比较笨重。如果要传递的参数过多,可以先将这些参数保存到一个slice中,再传递slice给min()函数。传递slice给函数的时候,使用SLICE...的方式即可。

func main() {    s1 := []int{30,40,50,60,70}    fmt.Println(min(10,20,s1...))}

上面的赋值方式已经能说明能使用slice来理解...的行为。另外,下面的例子也能很好的解释:

// 声明f1()func f1(s...string){    // 调用f2    f2    f3}// 声明f2func f2(s...string){}func f3(s []string){}

如果各参数的类型不同,又想定义成变长参数,该如何?第一种方式,可以使用struct,第二种方式可以使用接口。接口暂且不说,如果使用struct,大概如下:

type args struct {    arg1 string    arg2 int    arg3 type3}

然后可以将args传递给函数:f(a,b int,args{}),如果args结构中需要初始化,则f(a,b int,args{arg1:"hello",arg2:22})

    //len函数会获取map中键/值对的个数
    fmt.Println(“len:”, len(m))

   返回地址入栈

   
我们知道,当函数结束时,代码要返回到上一层函数继续执行,那么,函数如何知道该返回到哪个函数的什么位置执行呢?函数被调用时,会自动把下一条指令的地址压入堆栈,函数结束时,从堆栈读取这个地址,就可以跳转到该指令执行了。如果当前"call
foo"指令的地址是0x00171482,由于call指令占5个字节,那么下一个指令的地址为0x00171487,0x00171487将被压入堆栈:

图3

我也不知道为什么go要这么设计,我难道丢弃放回值不行吗?但是对于我们这个功能来说,必须要赋值的,因为append并没有修改原来的s2,它修改的是拷贝,append也是一个普通函数,对于slice也是传值进入的,传入24字节,append函数修改了作为参数复制的24字节,但是对于调用append的函数而言,那个slice已经和append内部使用的slice不是同一个24字节的内容,所以append需要返回一个slice对象,而对于调用者来说,最常见的用法是把这个传出参数,重新赋值给传入参数,即:s2
= append

0000000000448180 <runtime.duffzero>: 448180: 0f 11 07 movups %xmm0, 448183: 0f 11 47 10 movups %xmm0,0x10 448187: 0f 11 47 20 movups %xmm0,0x20 44818b: 0f 11 47 30 movups %xmm0,0x30 44818f: 48 83 c7 40 add $0x40,%rdi 448193: 0f 11 07 movups %xmm0, 448196: 0f 11 47 10 movups %xmm0,0x10 44819a: 0f 11 47 20 movups %xmm0,0x20 44819e: 0f 11 47 30 movups %xmm0,0x30 4481a2: 48 83 c7 40 add $0x40,%rdi ... 4482b0: c3 retq00000000004482c0 <runtime.duffcopy>: 4482c0: 0f 10 06 movups ,%xmm0 4482c3: 48 83 c6 10 add $0x10,%rsi 4482c7: 0f 11 07 movups %xmm0, 4482ca: 48 83 c7 10 add $0x10,%rdi 4482ce: 0f 10 06 movups ,%xmm0 4482d1: 48 83 c6 10 add $0x10,%rsi 4482d5: 0f 11 07 movups %xmm0, 4482d8: 48 83 c7 10 add $0x10,%rdi ... 448640: c3 retq 

    //数组类型是一维的,但是你可以通过组合创建多维数组结构
    var twoD [2][3]int
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            twoD[i][j] = i + j
        }
    }
    fmt.Println(“2d: “, twoD)
}

金沙js娱乐场官方网站 8堆栈的建立

    我们从main函数执行的第一行代码,即int result=foo(3,4);
开始跟踪。这时main以及之前的函数对应的堆栈帧已经存在在堆栈中了,如下图所示:

图1

useSlice的代码

前面提到的runtime.duffzero和runtime.duffcopy函数调用缺少参数指定内存大小的问题怎么解决吗?在这里不由callee不负责,而是由caller负责,caller根据要操作内存的大小来决定call到这两个函数的具体哪一条指令,对比通常的函数调用都是跳转到函数的入口地址,而对这个函数是跳转到函数内部的某一条指令,直到运行到RET指令为止,这期间运行了多少条拷贝赋值指令,就是操作了多大内存空间。

    //打印map会输出里面所有的键值对
    fmt.Println(“map:”, m)

通用寄存器入栈

    
最后,将函数中使用到的通用寄存器入栈,暂存起来,以便函数结束时恢复。在foo函数中用到的通用寄存器是EBX,ESI,EDI,将它们压入堆栈,如图所示:

图7

   至此,一个完整的堆栈帧建立起来了。

金沙js娱乐场官方网站 9堆栈特性分析

  
上一节中,一个完整的堆栈帧已经建立起来,现在函数可以开始正式执行代码了。本节我们对堆栈的特性进行分析,有助于了解函数与堆栈帧的依赖关系。

  
1)一个完整的堆栈帧建立起来后,在函数执行的整个生命周期中,它的结构和大小都是保持不变的;不论函数在什么时候被谁调用,它对应的堆栈帧的结构也是一定的。

  
2)在A函数中调用B函数,对应的,是在A函数对应的堆栈帧“下方”建立B函数的堆栈帧。例如在foo函数中调用foo1函数,foo1函数的堆栈帧将在foo函数的堆栈帧下方建立。如下图所示:

金沙js娱乐场官方网站 10

图8 

 
3)函数用EBP寄存器来访问参数和局部变量。我们知道,参数的地址总是比EBP的值高,而局部变量的地址总是比EBP的值低。而在特定的堆栈帧中,每个参数或局部变量相对于EBP的地址偏移总是固定的。因此函数对参数和局部变量的的访问是通过EBP加上某个偏移量来访问的。比如,在foo函数中,EBP+8为第一个参数的地址,EBP-8为第一个局部变量的地址。

  
4)如果仔细思考,我们很容易发现EBP寄存器还有一个非常重要的特性,请看下图中:

金沙js娱乐场官方网站 11

图9

  
我们发现,EBP寄存器总是指向先前的EBP,而先前的EBP又指向先前的先前的EBP,这样就在堆栈中形成了一个链表!这个特性有什么用呢,我们知道EBP+4地址存储了函数的返回地址,通过该地址我们可以知道当前函数的上一级函数(通过在符号文件中查找距该函数返回地址最近的函数地址,该函数即当前函数的上一级函数),以此类推,我们就可以知道当前线程整个的函数调用顺序。事实上,调试器正是这么做的,这也就是为什么调试时我们查看函数调用顺序时总是说“查看堆栈”了。

我们可以看到main函数把slice的三个成员全部通过堆栈传递给了useSlice,然后在useSlice里面在定义slice对象。

func useArray(ss [SIZE]int64) { ss[0] = 0x3333 44d700: 48 c7 44 24 08 33 33 movq $0x3333,0x8 44d707: 00 00 ss[SIZE-1] = 0x4444 44d709: 48 c7 84 24 80 00 00 movq $0x4444,0x80 44d710: 00 44 44 00 00}

    //使用内置的make来合建一个空的map,make(map[键类型]值类型)
    m := make(map[string]int)

金沙js娱乐场官方网站 12函数的调用约定(calling convention)

    函数的调用约定(calling
convention)指的是进入函数时,函数的参数是以什么顺序压入堆栈的,函数退出时,又是由谁(Caller还是Callee)来清理堆栈中的参数。有2个办法可以指定函数使用的调用约定:

    1)在函数定义时加上修饰符来指定,如

1 2 3 4 void __thiscall mymethod(); {     ... }

   
2)在VS工程设置中为工程中定义的所有的函数指定默认的调用约定:在工程的主菜单打开Project|Project
Property|Configuration Properties|C/C++|Advanced|Calling
Convention,选择调用约定(注意:这种做法对类成员函数无效)。

    常用的调用约定有以下3种:

   
1)__cdecl。这是VC编译器默认的调用约定。其规则是:参数从右向左压入堆栈,函数退出时由caller清理堆栈中的参数。这种调用约定的特点是支持可变数量的参数,比如printf方法。由于callee不知道caller到底将多少参数压入堆栈,因此callee就没有办法自己清理堆栈,所以只有函数退出之后,由caller清理堆栈,因为caller总是知道自己传入了多少参数。

    2)__stdcall。所有的Windows
API都使用__stdcall。其规则是:参数从右向左压入堆栈,函数退出时由callee自己清理堆栈中的参数。由于参数是由callee自己清理的,所以__stdcall不支持可变数量的参数。

   
3) __thiscall。类成员函数默认使用的调用约定。其规则是:参数从右向左压入堆栈,x86构架下this指针通过ECX寄存器传递,函数退出时由callee清理堆栈中的参数,x86构架下this指针通过ECX寄存器传递。同样不支持可变数量的参数。如果显式地把类成员函数声明为使用__cdecl或者__stdcall,那么,将采用__cdecl或者__stdcall的规则来压栈和出栈,而this指针将作为函数的第一个参数最后压入堆栈,而不是使用ECX寄存器来传递了。

func useSlice(ss []int64) { 467f60: 48 83 ec 08 sub $0x8,%rsp 467f64: 48 89 2c 24 mov %rbp, 467f68: 48 8d 2c 24 lea ,%rbp ss[0x11] = 0x21; 467f6c: 48 8b 44 24 18 mov 0x18,%rax # len value 467f71: 48 8b 4c 24 10 mov 0x10,%rcx # data pointer 467f76: 48 83 f8 21 cmp $0x11,%rax # 比较下标0x11和slice的len域,是否越界 467f7a: 77 02 ja 467f7e <main.useSlice+0x1e> 467f7c: eb 14 jmp 467f92 <main.useSlice+0x32> 467f7e: 48 c7 81 08 01 00 00 movq $0x22,0x88 # 把值0x22赋给slice[0x11] 467f85: 2c 00 00 00 467f89: 48 8b 2c 24 mov ,%rbp 467f8d: 48 83 c4 08 add $0x8,%rsp 467f91: c3 retq 

同时我们也能看到数组被定义的同时,也进行了初始化操作,调用runtime.duffzero函数,duffzero函数接收参数%rdi,把从%rdi地址开始的内存空间(在这个例子中就是数组ss的首地址)填满%xmm0的值,这个函数的设计很巧妙,后面我们介绍它。

    //从第二个(包括)字符开始截取到最后一个
    l = s[2:]
    fmt.Println(“sl3:”, l)

    为局部变量分配地址

   
接着,foo函数将为局部变量分配地址。程序并不是将局部变量一个个压入堆栈的,而是将ESP减去某个值,直接为所有的局部变量分配空间,比如在foo函数中有ESP=ESP-0x00E4,(根据烛秋兄在其他编译环境上的测试,也可能使用push命令分配地址,本质上并没有差别,特此说明)如图所示:

图5

    
奇怪的是,在debug模式下,编译器为局部变量分配的空间远远大于实际所需,而且局部变量之间的地址不是连续的(据我观察,总是间隔8个字节)如下图所示:

 图6

   
我还不知道编译器为什么这么设计,或许是为了在堆栈中插入调试数据,不过这无碍我们今天的讨论。

最后我们看一下汇编码,如何传递slice的

callq 0x...... <functionmame>

    //从开始截取到每5个字符(除了值)
    l = s[:5]
    fmt.Println(“sl2:”, l)

C/C++堆栈指引

Binhua Liu