数组越界怎么判断_GO语言圣经学习笔记(三)数组

忏悔录 Day90

数组实现原理

与大多数语言实现一样,数组是在内存中分配了一段连续的内存空间:97de110700eb6f7ecf7a1f800c7adfa6.png

func NewArray(elem *Type, bound int64) *Type {

if bound < 0 {

Fatalf("NewArray: invalid bound %v", bound)

}

t := New(TARRAY)

t.Extra = &Array{Elem: elem, Bound: bound}

t.SetNotInHeap(elem.NotInHeap())

return t

}

编译期间的数组类型是由上述的  cmd/compile/internal/types.NewArray 函数生成的,类型 Array 包含两个字段,一个是元素类型 Elem,另一个是数组的大小 Bound,这两个字段共同构成了数组类型,而当前数组是否应该在堆栈中初始化也在编译期就确定了。

数组初始化语法糖

[...]T{1,2,3}形式的数组大家应该都知道,这个其实是一个语法糖,实现方式也较为简单,删减源码如下:

func typecheckcomplit(n *Node) (res *Node) {

...

switch t.Etype {

case TARRAY, TSLICE:

var length, i int64

nl := n.List.Slice()

for i2, l := range nl {

i++

if i > length {

length = i

}

}

if t.IsDDDArray() {

t.SetNumElem(length)

}

}

}

DDDArray 指的就是此类数组(点 == Dot)

数组优化

对于一个由字面量组成的数组,根据数组元素数量的不同,编译器会在负责初始化字面量的  cmd/compile/internal/gc.anylit 函数中做两种不同的优化:

  1. 当元素数量小于或者等于 4 个时,会直接将数组中的元素放置在栈上;

  2. 当元素数量大于 4 个时,会将数组中的元素放置到静态区并在运行时取出;上代码:

func anylit(n *Node, var_ *Node, init *Nodes) {  t := n.Type  switch n.Op {  case OSTRUCTLIT, OARRAYLIT:    if n.List.Len() > 4 {      vstat := staticname(t)      vstat.Name.SetReadonly(true)      fixedlit(inNonInitFunction, initKindStatic, n, vstat, init)      a := nod(OAS, var_, vstat)      a = typecheck(a, ctxStmt)      a = walkexpr(a, init)      init.Append(a)      break    }    fixedlit(inInitFunction, initKindLocalCode, n, var_, init)  ...  }}func fixedlit(ctxt initContext, kind initKind, n *Node, var_ *Node, init *Nodes) {  var splitnode func(*Node) (a *Node, value *Node)  ...  for _, r := range n.List.Slice() {    a, value := splitnode(r)    a = nod(OAS, a, value)    a = typecheck(a, ctxStmt)    switch kind {    case initKindStatic:      genAsStatic(a)    case initKindLocalCode:      a = orderStmtInPlace(a, map[string][]*Node{})      a = walkstmt(a)      init.Append(a)    }  }}

访问和赋值

Go 语言中对越界的判断是可以在编译期间由静态类型检查完成的, cmd/compile/internal/gc.typecheck1 函数会对访问数组的索引进行验证:

func typecheck1(n *Node, top int) (res *Node) {  switch n.Op {  case OINDEX:    ok |= ctxExpr    l := n.Left  // array    r := n.Right // index    switch n.Left.Type.Etype {    case TSTRING, TARRAY, TSLICE:      ...      if n.Right.Type != nil && !n.Right.Type.IsInteger() {        yyerror("non-integer array index %v", n.Right)        break      }      if !n.Bounded() && Isconst(n.Right, CTINT) {        x := n.Right.Int64()        if x < 0 {          yyerror("invalid array index %v (index must be non-negative)", n.Right)        } else if n.Left.Type.IsArray() && x >= n.Left.Type.NumElem() {          yyerror("invalid array index %v (out of bounds for %d-element array)", n.Right, n.Left.Type.NumElem())        }      }    }  ...  }}

数组和字符串的一些简单越界错误都会在编译期间发现,比如我们直接使用整数或者常量访问数组,但是如果使用变量去访问数组或者字符串时,编译器就无法发现对应的错误了,这时就需要 Go 语言运行时发挥作用了:

arr[4]: invalid array index 4 (out of bounds for 3-element array)

arr[i]: panic: runtime error: index out of range [4] with length 3

Go 语言运行时在发现数组、切片和字符串的越界操作会由运行时的 panicIndex 和  runtime.goPanicIndex 函数触发程序的运行时错误并导致崩溃退出:

TEXT runtime·panicIndex(SB),NOSPLIT,$0-8  MOVL  AX, x+0(FP)  MOVL  CX, y+4(FP)  JMP  runtime·goPanicIndex(SB)func goPanicIndex(x int, y int) {  panicCheck1(getcallerpc(), "index out of range")  panic(boundsError{x: int64(x), signed: true, y: y, code: boundsIndex})}

当数组的访问操作 OINDEX 成功通过编译器的检查之后,会被转换成几个 SSA 指令,假设我们有如下所示的 Go 语言代码,通过如下的方式进行编译会得到 ssa.html 文件:

package checkfunc outOfRange() int {  arr := [3]int{1, 2, 3}  i := 4  elem := arr[i]  return elem}$ GOSSAFUNC=outOfRange go build array.godumped SSA to ./ssa.html

start 阶段生成的 SSA 代码就是优化之前的第一版中间代码,下面展示的部分就是 elem:=arr[i] 对应的中间代码,在这段中间代码中我们发现 Go 语言为数组的访问操作生成了判断数组上限的指令 IsInBounds 以及当条件不满足时触发程序崩溃的 PanicBounds 指令:

b1:    ...    v22 (6) = LocalAddr  {arr} v2 v20    v23 (6) = IsInBounds  v21 v11If v23 → b2 b3 (likely) (6)b2: ← b1-    v26 (6) = PtrIndex  v22 v21    v27 (6) = Copy  v20    v28 (6) = Load  v26 v27 (elem[int])    ...Ret v30 (+7)b3: ← b1-    v24 (6) = Copy  v20    v25 (6) = PanicBounds  [0] v21 v11 v24Exit v25 (6)

PanicBounds 指令最终会被转换成上面提到的 panicIndex 函数,当数组下标没有越界时,编译器会先获取数组的内存地址和访问的下标,然后利用 PtrIndex 计算出目标元素的地址,再使用 Load 操作将指针中的元素加载到内存中。

当然只有当编译器无法对数组下标是否越界无法做出判断时才会加入 PanicBounds 指令交给运行时进行判断,在使用字面量整数访问数组下标时就会生成非常简单的中间代码,当我们将上述代码中的 arr[i] 改成 arr[2] 时,就会得到如下所示的代码:

b1:

...

v21 (5) = LocalAddr 3]int> {arr} v2 v20

v22 (5) = PtrIndex int> v21 v14

v23 (5) = Load v22 v20 (elem[int])

...

Go 语言对于数组的访问还是有着比较多的检查的,它不仅会在编译期间提前发现一些简单的越界错误并插入用于检测数组上限的函数调用,而在运行期间这些插入的函数会负责保证不会发生越界错误。

数组的赋值和更新操作 a[i]=2 也会生成 SSA 生成期间计算出数组当前元素的内存地址,然后修改当前内存地址的内容,这些赋值语句会被转换成如下所示的 SSA 操作:

b1:

...

v21 (5) = LocalAddr 3]int> {arr} v2 v19

v22 (5) = PtrIndex int> v21 v13

v23 (5) = Store {int} v22 v20 v19

...

赋值的过程中会先确定目标数组的地址,再通过 PtrIndex 获取目标元素的地址,最后使用 Store 指令将数据存入地址中,从上面的这些 SSA 代码中我们可以看出无论是数组的寻址还是赋值都是在编译阶段完成的,没有运行时的参与。

引用

  • Go 语言设计与实现

  • 字面量

关于忏悔录

        忏悔录是我用来忏悔自己的一事无成,激励自己努力向前,督促自己以后要做好,的一系列文章吧,每天有所学,总结下来,可以是一章书本内容,一份感悟,不限内容。

        希望自己坚持住,尽量每天一更。

欢迎大家关注我的公众号

1678d1cdd79845e2ff7263bb4f28e605.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值