第 3 方 dll 中未处理的 delphi 异常

Unhandled delphi exception in 3rd party dll

我有一个动态库,我需要从中调用我的 Go 程序中的过程,但我似乎无法正确调用。我有另一个用 C# 编写的程序,它使用这个 DLL 并根据 dnSpy dllimport 编译为:

[DllImport("Library.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
    public static extern void Calc(double[] Input, [MarshalAs(UnmanagedType.VBByRefStr)] ref string Pattern, [MarshalAs(UnmanagedType.VBByRefStr)] ref string Database_path, double[] Output);

基于此,我尝试在我的代码中这样调用它:

func main() {
    lib := syscall.NewLazyDLL("Library.dll")
    proc := lib.NewProc("Calc")

    input := [28]float64{0.741, 0.585, 2, 12, 1, 1, 1, 101325, 2500, 3, 20, 17.73, 17.11, 45, 1, 0, 80, 60, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0}
    output := [20]float64{}
    tubePattern := marshalAnsi("P8-16G-E145/028")
    basePath, _ := filepath.Abs(".")
    databasePath := marshalAnsi(basePath)
    a1 := uintptr(unsafe.Pointer(&input))
    a2 := uintptr(unsafe.Pointer(tubePattern))
    a3 := uintptr(unsafe.Pointer(databasePath))
    a4 := uintptr(unsafe.Pointer(&output))
    ret, _, _ := proc.Call(a1, a2, a3, a4)
    log.Println(ret)
    log.Println(output)
}

func marshalAnsi(input string) *[]byte {
    var b bytes.Buffer
    writer := transform.NewWriter(&b, charmap.Windows1252.NewEncoder())
    writer.Write([]byte(input))
    writer.Close()
    output := b.Bytes()
    return &output
}

这导致未处理的 delphi 异常:

Exception 0xeedfade 0x31392774 0x32db04e4 0x7677ddc2
PC=0x7677ddc2

syscall.Syscall6(0x314af7d0, 0x4, 0x10ee7eb8, 0x10ed21a0, 0x10ed21d0, 0x10ee7d78, 0x0, 0x0, 0x0, 0x0, ...)
        C:/Go32/src/runtime/syscall_windows.go:184 +0xcf
syscall.(*Proc).Call(0x10ed21e0, 0x10ed85e0, 0x4, 0x4, 0x10, 0x49bee0, 0x533801, 0x10ed85e0)
        C:/Go32/src/syscall/dll_windows.go:152 +0x32e
syscall.(*LazyProc).Call(0x10ecb9c0, 0x10ed85e0, 0x4, 0x4, 0x0, 0x0, 0x4c6d70, 0x5491a0)
        C:/Go32/src/syscall/dll_windows.go:302 +0x48
main.main()
        C:/Users/Krzysztof Kowalczyk/go/src/danpoltherm.pl/dllloader/main.go:32 +0x2c0
eax     0x19f3f0
ebx     0x31392774
ecx     0x7
edx     0x0
edi     0x10ee7d78
esi     0x31392774
ebp     0x19f448
esp     0x19f3f0
eip     0x7677ddc2
eflags  0x216
cs      0x23
fs      0x53
gs      0x2b

我认为问题可能在于我将这些字符串传递给过程的方式,但我看不出我的代码与 C# 应用程序有什么不同。

库还有一些其他程序,我可以调用 DLL_Version 没有问题,如下所示:

lib := syscall.NewLazyDLL("Library.dll")
verProc := lib.NewProc("DLL_Version")
var version float64
verProc.Call(uintptr(unsafe.Pointer(&version)))
log.Printf("DLL version: %f\n", version)

输出:

2018/09/10 09:13:11 DLL version: 1.250000

编辑: 我相信我发现了导致异常的原因。似乎从我的代码调用时,此过程的第二个和第三个参数应该是指向字符串缓冲区的指针,而不是指向这些缓冲区上的指针的指针,如果我在过程的开头中断并手动修补它,代码运行并且正确终止。我仍然没有找到发生这种情况的原因,也没有找到解决方法。

编辑2: 显然我之前对这个案例的评估是无效的。我能够阻止程序抛出异常,但我分析了程序集,结果发现这只是由于程序在抛出异常之前退出,而不是异常我得到了关于无效字符串的正常错误代码。

虽然我不太了解反汇编代码,但看来问题确实可能是调用约定不匹配引起的。在过程代码的开头,当从工作程序调用时,EAX、EBX、ECX 和 EDX 寄存器为空,同时从我的代码调用时,只有 EAX 和 ECX 为空,EDX 保存一些东西,但 EBX 指向我的可执行文件中的 Calc 过程指针.稍后在 DLL 中有一部分检查 EBX 是否为空,如果不是则抛出异常。

编辑 3: 好吧,这似乎也无关紧要,我知道信息太少,任何人都无法猜测发生了什么。所以我会试着追踪这个异常的确切来源,然后回到这个问题。

您一直在犯一个相当常见的错误:将指针指向 Go 字符串或切片。这个问题是切片和字符串 are in fact structs——三个和两个字段,相应地。因此,当您获取包含切片或字符串的变量的地址时,这是该结构的第一个字段的地址, 而不是 容器中包含的第一个项目。

所以,第一个修复是替换所有使用

的地方
var v []whatever
p := unsafe.Pointer(&v)

var s string
p := unsafe.Pointer(&s)

var v []whatever
p := unsafe.Pointer(&v[0])

var s string
p := unsafe.Pointer(&s[0])

相应地。

还有两点需要考虑:

  • 我很难通过阅读措辞糟糕的 MSDN 文档来理解 VBByRefStr 的确切语义,但是通过粗略阅读一些关于它的帖子,我已经设法 google,您的目标函数似乎将这些参数键入为 PChar

    如果我的评估是正确的,您传递给函数的字符串必须以零结尾,因为它们不是 Delphi-native 字符串,并且其中没有编码的长度字段。

    因此,在准备字符串时,我会确保在末尾添加 \x00 字符。

    请注意,在您的示例中,Go 字符串是纯 ASCII(并注意 UTF-8 是 ASCII 干净的,即 UTF-8 编码的字符串仅包含 7 位 ASCII 字符,不需要任何重新编码在将它们传递给任何想要查看 ASCII 数据的东西之前),并且 Windows 代码页也与 ASCII 兼容。因此,对于您的开始,我不会搞乱适当的字符集转换,而是会做类似

    的事情
    func toPChar(s string) unsafe.Pointer {
      dst := make([]byte, len(s)+1)
      copy(dst, s)
      dst[len(s)] = 0 // NUL terminator
      return unsafe.Pointer(&dst[0])
    }
    

    …一旦你让它与纯 ASCII 字符串一起工作,在混合中添加字符转换。

  • 不要将 unsafe.Pointer 类型转换为 uintptr 的结果存储在变量中!

    这是一个不明显的事情,我承认,但逻辑如下:

    • 对于 Go GC,包含 unsafe.Pointer 类型值的变量构成对其指向的内存的实时引用(当然,如果该值不是 nil)。
    • uintptr 类型被专门定义为表示 不透明指针大小的整数 值,而不是指针。因此,存储在 uintptr 类型变量中的地址不算作对该地址处内存的实时引用。

这些规则意味着当您获取某个变量的地址并将其存储在类型 uintptr 的变量中时,这意味着您可能有零引用 到那个内存块。例如,在这个例子中

    func getPtr() unsafe.Pointer
    ...
    v := uintptr(getPtr())

GC 可以自由回收 getPtr() 返回的指针的内存。

作为一种特殊情况,Go 保证可以在表达式中使用 unsafe.Pointeruintptr 的转换,这些表达式被计算以产生实际参数来调用函数。因此,不要将中间指针存储为 uintptrs 而不是 unsafe.Pointers 并将它们转换为函数调用站点,如

    a1 := unsafe.Pointer(&whatever[0])
    ...
    ret, _, _ := proc.Call(uintptr(a1), uintptr(a2),
        uintptr(a3), uintptr(a4))

看到您正在调用的函数的实际 Delphi-native 类型也很棒。


另一个问题可能是调用约定不匹配,但如果我理解正确,P/Invoke 默认为 winapi CC(等于 Windows 上的 stdcall),并且因为你的 P/Invoke 包装器没有明确列出任何 CC,我们可能希望它确实是 winapi 所以我们在这里应该没问题。

好的,所以我终于找到了答案。正如@kostix 注意到 go-ole 是必需的,因为 DLL 使用 COM 并且我必须先调用 CoInitializeEx。

但这只是其中的一部分,事实证明过程的字符串参数被声明为 UnicodeString,这需要我将一个转换函数放在一起以正确适应文档中描述的布局:http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Unicode_in_RAD_Studio#New_String_Type:_UnicodeString

一旦完成,一切都开始按预期工作。