atomic.AddInt64导致无效的内存地址或空指针解引用。

huangapple go评论89阅读模式
英文:

atomic.AddInt64 Causes invalid memory address or nil pointer dereference

问题

在调用结构体字段上的atomic.AddInt64时,会出现“无效的内存地址或空指针解引用”错误,但在重新排列字段顺序后就不会出现这个错误;为什么会这样呢?

使用以下类型:

type CountHandler struct {
    c     *RequestContext
    count int64
}

调用atomic.AddInt64(&countHandler.count, 1)(此时字段c为nil)会导致错误。但是当我们将其重写为:

type CountHandler struct {
    count int64
    c     *RequestContext
}

错误就消失了。

我猜应该是这样的,因为Go在内存中按顺序保存数据,而访问nil值会打破这个(字节)序列;然而,我想知道为什么会这样,因为指针应该有一个固定的大小,无论是nil还是其他值。

这是在Windows上运行的Go x86 1.4.2,并且完整的错误消息如下:

2015/02/23 12:56:44 http: panic serving [::1]:51886: runtime error: invalid memory address or nil pointer dereference
goroutine 5 [running]:
net/http.func·011()
        c:/go/src/net/http/server.go:1130 +0xa8
sync/atomic.AddUint64(0x731144, 0x1, 0x0, 0x0, 0x263168)
        c:/go/src/sync/atomic/asm_386.s:118 +0xc
main.(*CountHandler).ServeHTTP(0x731140, 0x263180, 0x122f6380, 0x122f62a0)
        C:/Workshop/Devox/Workshop-Go/src/geoho/web/app/app.go:62 +0x42
github.com/julienschmidt/httprouter.func·001(0x263180, 0x122f6380, 0x122f62a0, 0x0, 0x0, 0x0)
        C:/Workshop/Devox/Workshop-Go/src/github.com/julienschmidt/httprouter/router.go:232 +0x4c
github.com/julienschmidt/httprouter.(*Router).ServeHTTP(0x122d5d20, 0x263180, 0x122f6380, 0x122f62a0)
        C:/Workshop/Devox/Workshop-Go/src/github.com/julienschmidt/httprouter/router.go:298 +0x141
net/http.serverHandler.ServeHTTP(0x122d2280, 0x263180, 0x122f6380, 0x122f62a0)
        c:/go/src/net/http/server.go:1703 +0x145
net/http.(*conn).serve(0x122e01e0)
        c:/go/src/net/http/server.go:1204 +0x9d8
created by net/http.(*Server).Serve
        c:/go/src/net/http/server.go:1751 +0x2ce

完整的源代码如下(此代码是错误的,我只是在研究alice):

package main

import (
    "fmt"
    "github.com/julienschmidt/httprouter"
    "github.com/justinas/alice"
    "net/http"

    "os"
    "sync/atomic"
)

// play with alice
func main() {
    c1 := alice.New(Counter, Texter).Then(nil)

    router := httprouter.New()
    router.Handler("GET", "/", c1)
    router.GET("/kill", kill)

    http.ListenAndServe(":27007", router)
}

func kill(rw http.ResponseWriter, rq *http.Request, pl httprouter.Params) {
    os.Exit(0)
}

var ch CountHandler

// constructors:

func Counter(h http.Handler) http.Handler {
    return &ch
}

func Texter(h http.Handler) http.Handler {
    var t TextHandler
    switch x := h.(type) {
    case *CountHandler:
        t.c = x.c
        t.text = fmt.Sprintf("called so far %d", atomic.LoadInt64(&x.count))
    }
    return &t
}

// handlers:

type RequestContext struct {
    val int
}

type CountHandler struct {
    c     *RequestContext
    count int64
}

func (c *CountHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
    atomic.AddInt64(&c.count, 1)
}

type TextHandler struct {
    c    *RequestContext
    text string
}

func (t *TextHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
    rw.Write([]byte(t.text))
}
英文:

Calling atomic.AddInt64 on field of a struct panics invalid memory address or nil pointer dereference, but not when we re-arrange fields order; why?

Using this type:

type CountHandler struct {
c     *RequestContext
count int64
}

And calling atomic.AddInt64(&countHandler.count, 1) (field c is nil at this point) panics. But not when we rewrite it as:

type CountHandler struct {
count int64
c     *RequestContext
}

Error goes away.

I guess it should be so, because Go keeps data in memory in a sequential manner and reaching a nil value breaks this sequence (of bytes); yet I wonder why is that so again because a pointer should have a fixed size nil or other value.

This is Go x86 1.4.2 on Windows & complete error message is:

2015/02/23 12:56:44 http: panic serving [::1]:51886: runtime error: invalid memory address or nil pointer dereference
goroutine 5 [running]:
net/http.func·011()
c:/go/src/net/http/server.go:1130 +0xa8
sync/atomic.AddUint64(0x731144, 0x1, 0x0, 0x0, 0x263168)
c:/go/src/sync/atomic/asm_386.s:118 +0xc
main.(*CountHandler).ServeHTTP(0x731140, 0x263180, 0x122f6380, 0x122f62a0)
C:/Workshop/Devox/Workshop-Go/src/geoho/web/app/app.go:62 +0x42
github.com/julienschmidt/httprouter.func·001(0x263180, 0x122f6380, 0x122f62a0, 0x0, 0x0, 0x0)
C:/Workshop/Devox/Workshop-Go/src/github.com/julienschmidt/httprouter/router.go:232 +0x4c
github.com/julienschmidt/httprouter.(*Router).ServeHTTP(0x122d5d20, 0x263180, 0x122f6380, 0x122f62a0)
C:/Workshop/Devox/Workshop-Go/src/github.com/julienschmidt/httprouter/router.go:298 +0x141
net/http.serverHandler.ServeHTTP(0x122d2280, 0x263180, 0x122f6380, 0x122f62a0)
c:/go/src/net/http/server.go:1703 +0x145
net/http.(*conn).serve(0x122e01e0)
c:/go/src/net/http/server.go:1204 +0x9d8
created by net/http.(*Server).Serve
c:/go/src/net/http/server.go:1751 +0x2ce

Whole source code is (this code is wrong, I was just about studying alice):

package main
import (
"fmt"
"github.com/julienschmidt/httprouter"
"github.com/justinas/alice"
"net/http"
"os"
"sync/atomic"
)
// play with alice
func main() {
c1 := alice.New(Counter, Texter).Then(nil)
router := httprouter.New()
router.Handler("GET", "/", c1)
router.GET("/kill", kill)
http.ListenAndServe(":27007", router)
}
func kill(rw http.ResponseWriter, rq *http.Request, pl httprouter.Params) {
os.Exit(0)
}
var ch CountHandler
// constructors:
func Counter(h http.Handler) http.Handler {
return &ch
}
func Texter(h http.Handler) http.Handler {
var t TextHandler
switch x := h.(type) {
case *CountHandler:
t.c = x.c
t.text = fmt.Sprintf("called so far %d", atomic.LoadInt64(&x.count))
}
return &t
}
// handlers:
type RequestContext struct {
val int
}
type CountHandler struct {
c     *RequestContext
count int64
}
func (c *CountHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
atomic.AddInt64(&c.count, 1)
}
type TextHandler struct {
c    *RequestContext
text string
}
func (t *TextHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(t.text))
}

答案1

得分: 14

故障在第一种情况下是由于原子更新的字段未正确对齐引起的。

在ARM和x86-32上,调用者有责任确保以原子方式访问的64位字的64位对齐。全局变量或分配的结构体或切片中的第一个字可以保证是64位对齐的。

英文:

The fault is caused in the first case by the atomically updated field not being properly aligned.

> On both ARM and x86-32, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a global variable or in an allocated struct or slice can be relied upon to be 64-bit aligned.

答案2

得分: 1

以下是解决该问题的一些技巧,以防您遇到此错误:

最简单的方法是按照 OP 中的解释,将所有的 64 位原子值放在结构体的顶部:

c := struct {
    val   int64 // 位置 0
    val2  int64 // 位置 8
    valid bool  // 位置 16
}{val2: 1}
fmt.Println(atomic.AddInt64(&c.val2, 1))

如果出于某种原因,您不想将该字段放在顶部,您可以在 64 位字段上方始终放置一个 _ [4]byte,以确保它正确对齐。

c := struct {
    val   int64   // 位置 0
    valid bool    // 位置 8
    _     [4]byte // 位置 9;编译器在位置 13 添加额外的 [3]byte 进行对齐
    val2  int64   // 位置 16,正确对齐
}{val2: 1}
fmt.Println(atomic.AddInt64(&c.val2, 1)) // => 2

请注意,如果字段已经对齐,这种方法将不起作用;相反,如果之前没有发生 panic,现在将会发生 panic。

c := struct {
    val   int64   // 位置 0
    _     [4]byte // 位置 8;编译器不添加填充
    val2  int64   // 位置 12,不是 8 的倍数!
}{val2: 1}
fmt.Println(atomic.AddInt64(&c.val2, 1)) // => 运行时错误:无效的内存地址 [...]

您还可以依赖于切片中的 64 位元素的第一个元素将被正确对齐的行为:

c := struct {
    val   int64
    valid bool
    val2  []int64
}{val2: []int64{1}}
fmt.Println(atomic.AddInt64(&c.val2[0], 1))

请注意,对于数组来说,这种方法不起作用,因为它们的值直接存储在结构体中,而不是像切片数据一样存储在堆上。

您还可以将结构体中的字段声明为指向 int64 的指针,如果它指向的 int64 对齐,那么它将正常运行。

c := struct {
    val   int64
    valid bool
    val2  *int64
}{val2: new(int64)}
fmt.Println(atomic.AddInt64(c.val2, 1))

如果您不想亲自处理 sync/atomic,请记住 sync.Mutex 比处理原子操作更清晰和易懂。

英文:

Here are a few tricks to solve the problem in case you stumble upon this error:

The easiest way, as explained in OP, is to place all your 64-bit atomic values at the top of the struct:

c := struct {
val   int64 // pos 0
val2  int64 // pos 8
valid bool  // pos 16
}{val2: 1}
fmt.Println(atomic.AddInt64(&c.val2, 1))

If you, for whatever reason, don't want to place this field at the top, you can always place a _ [4]byte above the 64-bit field to ensure it is correctly padded.

c := struct {
val   int64   // pos 0
valid bool    // pos 8
_     [4]byte // pos 9; compiler adds additional [3]byte at pos 13 for alignment
val2  int64   // pos 16, correctly aligned
}{val2: 1}
fmt.Println(atomic.AddInt64(&c.val2, 1)) // => 2

Note that this will not work in case the field was already aligned; on the contrary, if it didn't panic before, it will panic now.

c := struct {
val   int64   // pos 0
_     [4]byte // pos 8; compiler adds no padding
val2  int64   // pos 12, not a multiple of 8!
}{val2: 1}
fmt.Println(atomic.AddInt64(&c.val2, 1)) // => runtime error: invalid memory address [...]

You can also rely on the behaviour that the first element in slices of 64-bit elements will be correctly aligned:

c := struct {
val   int64
valid bool
val2  []int64
}{val2: []int64{1}}
fmt.Println(atomic.AddInt64(&c.val2[0], 1))

Note that this won't work for arrays, as their values are stored in the struct directly and not on the heap as it is the case for slice data.

The last trick you can pull off is that of declaring the field in the struct as a pointer to an int64; if the int64 it points to is aligned, then it will run smoothly.

c := struct {
val   int64
valid bool
val2  *int64
}{val2: new(int64)}
fmt.Println(atomic.AddInt64(c.val2, 1))

If you don't want to get your hands dirty with sync/atomic, remember that sync.Mutex is a much cleaner and more understandable solution than dealing with atomics.

huangapple
  • 本文由 发表于 2015年2月23日 17:13:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/28670232.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定