首页 > 解决方案 > 通过 cgo 调用 fts_open 时出现分段违规错误

问题描述

我正在测试 cgo,每个简单的 hello world 都像代码一样运行良好。
但我对下面的 C 代码有疑问。
C代码是遍历目录树并对文件大小求和。
如果我使用 go 命令构建,那么构建是可以的,没有错误。
但是在运行时,发生了“分段冲突”错误

bash$./walkdir 
fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x1 addr=0x1 pc=0x7f631e077c1a]
. . . .

-------------------------------------------------------------

package main
/*
#include <stdint.h>
#include <fts.h>
#include <sys/stat.h>

uintmax_t get_total_size(char *path)
{
    uintmax_t total_size = 0;
    FTS *fts = fts_open(&path, FTS_PHYSICAL, NULL);
    FTSENT *fent;
    while ((fent = fts_read(fts)) != NULL)
        if (fent->fts_info == FTS_F)
            total_size += fent->fts_statp->st_size;
    fts_close(fts);
    return total_size;
}
*/
import "C"
import "fmt"

func main() {
    fmt.Println(C.get_total_size(C.CString("/usr")))
}

标签: gofficgo

解决方案


fts_open 定义如下:

fts_open()
fts_open()函数接受一个指向字符指针数组的指针,该数组命名一个或多个路径,这些路径构成要遍历的逻辑文件层次结构。数组必须由 null指针终止。

C 不直接支持数组;它只有指针。在您的情况下,您传递fts_open了一个有效的指针,但它不位于将NULL指针作为紧随其后的元素的数组中,因此fts_open继续扫描过去的内存&path- 寻找NULL指针,并最终尝试在某个地址读取内存禁止这样做(通常是因为该地址处的页面未分配)。

修复它的一种方法是创建该数组并在 C 端对其进行初始化。
看起来您正在使用相当最新的 C 标准,所以让我们使用直接文字来初始化数组:

package main

/*
#include <stddef.h> // for NULL
#include <stdint.h>
#include <stdlib.h> // for C.free
#include <fts.h>
#include <sys/stat.h>

uintmax_t get_total_size(char *path)
{
    uintmax_t total_size = 0;
    char * path_argv[2] = {path, NULL};
    FTS *fts = fts_open(path_argv, FTS_PHYSICAL, NULL);
    FTSENT *fent;
    while ((fent = fts_read(fts)) != NULL)
        if (fent->fts_info == FTS_F)
            total_size += fent->fts_statp->st_size;
    fts_close(fts);
    return total_size;
}
*/
import "C"

import (
    "fmt"
    "unsafe"
)

func main() {
    cpath := C.CString("/usr")
    defer C.free(unsafe.Pointer(cpath))
    fmt.Println(C.get_total_size(cpath))
}

请注意,您的程序有一个错误和一个可能的问题:

  • 一个错误是调用通过从链接的 C 库C.CString执行调用来分配一块内存malloc(3),而您没有释放该内存块。
  • 该符号NULL在“stddef.h”中定义;编译时您可能会或可能不会收到错误。

我已经在我的示例中解决了这两个问题。

对我们示例的进一步改进可能是利用fts_*函数在一次运行中扫描多个路径的能力;fts_open如果我们要实现它,为 Go 的第一个参数分配数组会更有意义:

package main

/*
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <fts.h>
#include <sys/stat.h>

uintmax_t get_total_size(char * const *path_argv)
{
    uintmax_t total_size = 0;
    FTS *fts = fts_open(path_argv, FTS_PHYSICAL, NULL);
    FTSENT *fent;
    while ((fent = fts_read(fts)) != NULL)
        if (fent->fts_info == FTS_F)
            total_size += fent->fts_statp->st_size;
    fts_close(fts);
    return total_size;
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    fmt.Println(getTotalSize("/usr", "/etc"))
}

func getTotalSize(paths ...string) uint64 {
    argv := make([]*C.char, len(paths)+1)
    for i, path := range paths {
        argv[i] = C.CString(path)
        defer C.free(unsafe.Pointer(argv[i]))
    }

    return uint64(C.get_total_size(&argv[0]))
}

请注意,这里我们没有明确地将 的最后一个参数清零,argv因为与 C 相反,Go 用零初始化每个分配的内存块,所以一旦argv分配,它的所有内存都已经清零了。


推荐阅读