Golang与C交互时崩溃问题排查
Golang与C交互时崩溃问题排查
下面的代码使用 C 操作符 &xx->out 来访问嵌入结构体的第一个条目,而 GO 使用 &xx.out 来完成相同的任务 → 问题在于 GO 会评估 xx,而 C 不会。如果你查看汇编代码:Compiler Explorer,C 编译器将 &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
根据你的示例以及你提到的对象模型,听起来你正试图将一个基于继承的面向对象模型移植到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:
- 如果空指针测试进入“调用者”,应用程序逻辑会改变
无论如何,改变应用程序逻辑难道不是一个好主意吗?试想一下,如果你(或其他人)重构了 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在语法语义上的重要区别。


