Golang与C交互时崩溃问题排查

Golang与C交互时崩溃问题排查 下面的代码使用 C 操作符 &xx->out 来访问嵌入结构体的第一个条目,而 GO 使用 &xx.out 来完成相同的任务 → 问题在于 GO 会评估 xx,而 C 不会。如果你查看汇编代码:Compiler ExplorerC 编译器将 &xx->out 翻译成一个简单的指针转换,没有任何开销,而 GO 似乎忽略了这一事实。

以下 C 代码运行正常:

#include <stdlib.h>
#include <stdio.h>

struct base {
    int b;
};

struct A {
    struct base obj;
    int a;
};

void method_base (struct base* hdl, int val) {
    if (hdl == NULL) {
      fprintf(stderr,"sorry.\n");
    } else {
      hdl->b = val;
      fprintf(stderr,"set: hdl->b = %d\n", hdl->b);
    }
}

int main(int argc, char** argv) {

    fprintf(stderr,"init: aO = calloc\n");
    struct A * aO = (struct A*) calloc(1,sizeof(*aO));
    fprintf(stderr,"init: bO = NULL\n");
    struct A * bO = NULL;

    fprintf(stderr,"step: cast aO\n");
    method_base((struct base*) aO, 1);
    fprintf(stderr,"step: ref aO\n");
    method_base(&aO->obj, 2);
    fprintf(stderr,"step: cast bO\n");
    method_base((struct base*) bO, 3);
    fprintf(stderr,"step: ref bO\n");
    method_base(&bO->obj, 4);
    fprintf(stderr,"step: end.\n");
}

输出:

init: aO = calloc init: bO = NULL step: cast aO set: hdl->b = 1 step: ref aO set: hdl->b = 2 step: cast bO sorry. step: ref bO sorry. step: end.

以下 GO 代码崩溃

package main

// #include <stdlib.h>
// #include <stdio.h>
// struct base {
//     int b;
// };
// struct A {
//     struct base obj;
//     int a;
// };
// void method_base (struct base* hdl, int val) {
//     if (hdl == NULL) {
//       fprintf(stderr,"sorry.\n");
//     } else {
//       hdl->b = val;
//       fprintf(stderr,"set: hdl->b = %d\n", hdl->b);
//     }
// }
import "C"
import "fmt"
import "unsafe"
import "os"

func main() {

    fmt.Fprintf(os.Stderr,"init: aO = calloc\n");
    aO := (*C.struct_A)(C.calloc(1,C.sizeof_struct_A));
    fmt.Fprintf(os.Stderr,"init: bO = NULL\n");
    bO := (*C.struct_A)(C.NULL);

    fmt.Fprintf(os.Stderr,"step: cast aO\n");
    C.method_base((*C.struct_base)(unsafe.Pointer(aO)), 1);
    fmt.Fprintf(os.Stderr,"step: ref aO\n");
    C.method_base(&aO.obj, 2);
    fmt.Fprintf(os.Stderr,"step: cast bO\n");
    C.method_base((*C.struct_base)(unsafe.Pointer(bO)), 3);
    fmt.Fprintf(os.Stderr,"step: ref bO\n");
    C.method_base(&bO.obj, 4);
    fmt.Fprintf(os.Stderr,"step: end.\n");
}

输出:

init: aO = calloc init: bO = NULL step: cast aO set: hdl->b = 1 step: ref aO set: hdl->b = 2 step: cast bO sorry. step: ref bO panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x48fbb4]

goroutine 1 [running]: main.main() …/main.go:39 +0x224


更多关于Golang与C交互时崩溃问题排查的实战教程也可以访问 https://www.itying.com/category-94-b0.html

7 回复

问题在于……

  1. 我希望避免指针的“类型转换”……并且
  2. 如果将空指针测试移到“调用者”处,应用程序的逻辑会发生变化

更多关于Golang与C交互时崩溃问题排查的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


根据你的示例以及你提到的对象模型,听起来你正试图将一个基于继承的面向对象模型移植到Go语言中。我认为你需要考虑使用其他语言,或者需要对你的代码进行重大重构,因为Go语言在设计上故意省略了继承机制。

嗨,@aotto1968,我明白你的意思,但根据我的经验,Go语言对NULL/nil指针的处理方式(在我看来)有些特殊(可以参考这个例子)。

话虽如此,我很好奇这对你来说为什么会成为一个问题。

“obj” 必须是结构体中的第一个成员 → 这是一个“对象模型”,目标是使两个指针 “A” 和 “base” 指向相同的位置。如果你熟悉 C++ 语言,那么 “base” 就是 “A” 的“基类”,并且 “method_base” 既可以通过 “base” 指针调用,也可以通过 “A” 指针调用。

看起来Ian Lance Taylor在golang-nuts中给出了一个答案:重定向到Google Groups

(为方便起见,复制在此:)

C语言允许你获取NULL指针偏移量的地址。 Go语言不允许你获取nil指针偏移量的地址,即使偏移量为零也不行。

aotto1968:

  1. 如果空指针测试进入“调用者”,应用程序逻辑会改变

无论如何,改变应用程序逻辑难道不是一个好主意吗?试想一下,如果你(或其他人)重构了 struct A 以添加一个头部信息。那么 obj 的偏移量就会变成 4 或 8,而空指针检查将无法检测到这个错误。

struct A

问题出在Go对&bO.obj的处理上。当bO为nil时,Go会在解引用时立即触发panic,而C的&bO->obj只是进行地址计算。

在C中,&bO->obj等价于(struct base*)bO(因为obj是第一个字段),即使bO为NULL也不会解引用。但Go的&bO.obj需要先评估bO,导致nil指针解引用。

解决方案是手动进行指针转换,避免Go的自动解引用:

package main

// #include <stdlib.h>
// #include <stdio.h>
// struct base {
//     int b;
// };
// struct A {
//     struct base obj;
//     int a;
// };
// void method_base (struct base* hdl, int val) {
//     if (hdl == NULL) {
//       fprintf(stderr,"sorry.\n");
//     } else {
//       hdl->b = val;
//       fprintf(stderr,"set: hdl->b = %d\n", hdl->b);
//     }
// }
import "C"
import "fmt"
import "unsafe"
import "os"

func main() {
    fmt.Fprintf(os.Stderr,"init: aO = calloc\n")
    aO := (*C.struct_A)(C.calloc(1,C.sizeof_struct_A))
    fmt.Fprintf(os.Stderr,"init: bO = NULL\n")
    bO := (*C.struct_A)(C.NULL)

    fmt.Fprintf(os.Stderr,"step: cast aO\n")
    C.method_base((*C.struct_base)(unsafe.Pointer(aO)), 1)
    fmt.Fprintf(os.Stderr,"step: ref aO\n")
    C.method_base(&aO.obj, 2)
    fmt.Fprintf(os.Stderr,"step: cast bO\n")
    C.method_base((*C.struct_base)(unsafe.Pointer(bO)), 3)
    fmt.Fprintf(os.Stderr,"step: ref bO - fixed\n")
    // 手动转换,避免Go解引用nil指针
    C.method_base((*C.struct_base)(unsafe.Pointer(bO)), 4)
    fmt.Fprintf(os.Stderr,"step: end.\n")
}

关键修改在第39行:用(*C.struct_base)(unsafe.Pointer(bO))替代&bO.obj。这样直接进行指针转换,不涉及对bO的解引用操作。

对于非nil指针,两种写法都有效:

  • &aO.obj 正常工作
  • (*C.struct_base)(unsafe.Pointer(aO)) 也正常工作

但对于nil指针,只能使用指针转换方式。这是Go和C在语法语义上的重要区别。

回到顶部