第 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 struct
s——三个和两个字段,相应地。因此,当您获取包含切片或字符串的变量的地址时,这是该结构的第一个字段的地址, 而不是 容器中包含的第一个项目。
所以,第一个修复是替换所有使用
的地方
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.Pointer
到 uintptr
的转换,这些表达式被计算以产生实际参数来调用函数。因此,不要将中间指针存储为 uintptr
s 而不是 unsafe.Pointer
s 并将它们转换为函数调用站点,如
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
一旦完成,一切都开始按预期工作。
我有一个动态库,我需要从中调用我的 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 struct
s——三个和两个字段,相应地。因此,当您获取包含切片或字符串的变量的地址时,这是该结构的第一个字段的地址, 而不是 容器中包含的第一个项目。
所以,第一个修复是替换所有使用
的地方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
类型变量中的地址不算作对该地址处内存的实时引用。
- 对于 Go GC,包含
这些规则意味着当您获取某个变量的地址并将其存储在类型 uintptr
的变量中时,这意味着您可能有零引用
到那个内存块。例如,在这个例子中
func getPtr() unsafe.Pointer
...
v := uintptr(getPtr())
GC 可以自由回收 getPtr()
返回的指针的内存。
作为一种特殊情况,Go 保证可以在表达式中使用 unsafe.Pointer
到 uintptr
的转换,这些表达式被计算以产生实际参数来调用函数。因此,不要将中间指针存储为 uintptr
s 而不是 unsafe.Pointer
s 并将它们转换为函数调用站点,如
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
一旦完成,一切都开始按预期工作。