go

go cgo

go cgo访问空指针不报段错误,静默越过程序

Posted by Liangjf on December 31, 2019

go cgo访问空指针不报段错误,静默越过程序

demo目录结构:

main

package main

/*
#cgo CFLAGS : -I./include
#cgo LDFLAGS: -L./lib -ltest
#include "test.h"
*/
import "C"

import "fmt"

func main() {
    p := C.hello(C.CString("laingjf"))
    fmt.Println(C.GoString(p))
    fmt.Println("p1 ", p)
    defer func() {
        C.freeMemory(p)
    }()
}

test.c

#include"test.h"
char* hello(const char* name)
{
    printf("hello1\n");
    printf("name %p\n", name);
    printf("hello2\n");
    const char* hello=" -> hello";
    char* result=(char*)malloc(sizeof(char)*(strlen(name))+strlen(hello));
    printf("result %p\n", result);
    strcpy(result,name);
    strcat(result,hello);
    return result;
}

void freeMemory(char* p)
{
    if(p != NULL)
    {
        free(p);
        p = NULL;
        printf("free memory\n");
        printf("free p %p\n", p);
    }
}

test.h

#ifndef TEST_H
#define TEST_H

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

char* hello(const char* name);
void freeMemory(char* p);

#endif

makelib.sh

#!/bin/bash
gcc -fPIC -shared -o lib/libtest.so c_src/test.c -I./includeexport 
LD_LIBRARY_PATH=~/ljf_home/code/go_home/project/go-utils/cgo/demo3/lib

执行命令:

./makeilb.sh
go run main.go

输出:

hello1
name 0x86f7e0
hello2
result 0x86fc10
laingjf -> hello
p1  0x86fc10
free memory
free p (nil)

上面的是标准的demo,c局部函数申请内存,外部通过go的 defer 保证最终效释放内存,避免了内存泄漏。(网上抄来抄去那个例子是内存泄漏的)。

main

package main

/*
#cgo CFLAGS : -I./include
#cgo LDFLAGS: -L./lib -ltest
#include "test.h"
*/
import "C"

import "fmt"

func main() {
    p := C.hello(C.CString("laingjf"))
    fmt.Println(C.GoString(p))
    fmt.Println("p1 ", p)
    defer func() {
        C.freeMemory(p)
        fmt.Println("p2 ", p)
        fmt.Println(C.GoString(p))
        fmt.Println("-------")
    }()
}

修改main.go,在调用释放内存函数后,继续访问p指针,执行命令,输出如下:

hello1
name 0x21177e0
hello2
result 0x2117c10
laingjf -> hello
p1  0x2117c10
free memory
free p (nil)
p2  0x2117c10

-------

注意,注意,注意:只是静默的越过NULL指针,没有报段错误。释放的只是c申请的内存,go这边的是gc控制的,所以在打印p2看到的指针地址还是原来未释放的地址

下面看c语言对NULL指针的处理:

main.c

#include <stdio.h>

int main()
{
    char *p = NULL;
    printf("%s\n", p);
    return 0;
}

执行:

gcc main.c
./a.out

输出: 段错误

通过演示,看到,go cgo访问空指针不报段错误,静默越过。关键点在于是Go指针还是C指针,是根据内存如何分配判断的,与指针的类型无关

在官方文档中关于go和c与内存有关的介绍:

  • Go指针指的是指向Go分配的内存的指针(例如使用&运算符或者调用new函数获取的指针)
  • C指针指的是C分配的内存的指针(例如调用malloc函数获取的指针)。
  • 一个指针是Go指针还是C指针,是根据内存如何分配判断的,与指针的类型无关。

Go调用C

  • 传递指向Go Memory的指针。(Go调用C Code时,Go传递给C Code的Go指针所指的Go Memory中不能包含任何指向Go Memory的Pointer)
  • 传递指向struc field的指针。(Go调用C Code时,如果传递的是一个指向struct field的指针,那么“Go Memory”专指这个field所占用的内存,即便struct中有其他field指向其他Go Memory也没关系)
  • 传递指向slice或array中的element指针。(传递一个指向slice或者array中的element指针时,需要考虑的Go Memory的范围不仅仅是这个element,而是整个array或这个slice背后的underlying array所占用的内存区域,要保证整个区域内不包含任何指向Go Memory的指针)

C调用Go

  • 返回指向Go分配的内存的指针。(C调用的Go函数不能返回指向Go分配的内存的指针)
  • 在C分配的内存中存储指向Go分配的内存的指针。(Go Code不能在C分配的内存中存储指向Go分配的内存的指针)

原理:cgo中,Go与C的内存应该保持着相对独立,指针之间的传递应该尽量避免嵌套不同内存的指针。他们是分开管理的。凡是涉及到内存分配的操作,应该独立管理。