惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

Razeen`s Blog

Let's Encrypt 推出 Gen Y 根证书架构:揭示 Web PKI 的五大未来趋势 通过 Wi-Fi 自动备份你的 iPhone 到 Nas 管理培训感悟:从技术视角看管理之路 UPS 一拖多:保护你的 NAS 和 Linux 服务器 从影音中心到 AI 助手:我的50款 Homelab 服务清单 从SSL证书有效期将缩短到47天聊开去 部署一个自己的 Running Page 谈谈特斯拉 Model Y 用车一年的感受和费用分析 弃用 Disqus 评论,使用自建 Waline 使用 Prometheus 和 Grafana 搭建你的证书监控面板 服务器迁移记:一次磁盘换板引发的Linux分区与挂载的学习 改善信息来源, 利用 RSS 高效获取资讯 (RSShub + Reeder5 + WeWe RSS) 多种 Docker 镜像拉取解决方案与实践 App分享 | AppCleaner - Mac上的卸载神器 Azure OpenAI API 申请和使用 如何开通 OneKey 虚拟信用卡,并充值消费 如何拥有一个可长期在国内使用的国外手机号码 分享开通 ChatGPT Plus 过程 如何开通 Depay 虚拟信用卡,并充值消费 如何开通欧易Web3钱包, 交易入账 如何5分钟内1块钱注册 ChatGPT 科学上网 如何注册美区的 Apple ID (2023年/无需科学上网) NAS折腾记(11): NASTool3.0体验和降级 NAS折腾记(10): Docker版本的NASTool配置 NAS折腾记(9): NASTool与微信交互,微信发送消息远程下载电影【多图】 NAS折腾记(8):群晖安装 NASTool 实现影音半自动化【多图】 内网穿透(2):Tailscale 组网实现内网穿透,操作简单,无成本 内网穿透(1):总结了11中内网穿透的方式,总有一种适合你 NAS折腾记(7):从零开始设置(黑)群晖系统 NAS折腾记(6):黑群晖系统安装好后怎么洗白? NAS折腾记(5):群晖硬盘休眠设置与分析 NAS折腾记(4): 20分钟手把手带你完美安装 DS918+ 黑群晖7.1.1 NAS折腾记(3):NAS 装机 NAS折腾记(2):B360-ITX 双M.2 双2.5G网口 6 SATA主版 开箱 NAS折腾记(1):328元的4盘位Nas机箱开箱 Openwrt + Clash 全局科学上网 Newifi3 刷入 OpenWrt 固件 v21.02 利用 Markdown 画一些流程图、时序图、甘特图等 Homelab(8): 搭建自用 Gitlab 与 Docker 仓库 Nginx Tcp 转发保留客户端真实 IP (PROXY Protocol) Homelab (7):IPSec VPN(基于证书认证)客户端设置 Homelab (6): 基于自签发证书的 IPSec VPN 搭建 Homelab (5): DDNS 动态域名解析 Homelab (4): Linux 服务器基础环境准备 Homelab (3): 整体网络与基础硬件介绍 Go学习笔记(十)老项目迁移 go module 大型灾难记录 Github Actions 初体验之自动化部署 Hexo 博客 记一次 Nginx DNS 缓存导致转发问题 Homelab (2): 电信悦me网关修改桥接模式,路由器拨号 Homelab (1):5分钟上手黑群晖 NAS 终极 Bash 脚本指南 Typora 自动上传图片到七牛云 Ubuntu 20.04 LTS 有线网卡驱动安装 折腾 Ubuntu 20.04 LTS 开发环境 Go学习笔记(九) 计时器的生命周期[译] 利用 git hook 规范你的代码与 commit message 规范 git commit message 与自动化版本控制 超详细 vim 配置 (with MacVim) Golang 中的 RESTful API 最佳实践 折腾服务器(开篇) 我的第一台个人服务器 Newifi3 实现低成本家庭级科学上网 Go学习笔记(八) | 使用 os/exec 执行命令 Mac OS 自动根据 WI-FI 名字改变网络位置 关于 Docker 清理 MIME Types 速查表 Go学习笔记(七) | 理解OAuth 2.0并实现一个客户端 我又又又把博客迁移了 Go学习笔记(六) | 使用swaggo自动生成Restful API文档 Go学习笔记(五) | 使用代码片段(snippets)提高编码效率 书单 - 2018 IPFS 初体验,利用 IPFS 托管你的静态网站 记一次 PostgreSQL LIKE 索引优化,联合字段 LIKE 查询优化。 Disqus 添加有趣的 Reactions 的功能 TLS 1.3 详解 (RFC 8446解读) gRPC在Go中的使用(三)gRPC实现TLS加密通信与流模式 gRPC在Go中的使用(二)gRPC实现简单通讯 gRPC在Go中的使用(一)Protocol Buffers语法与相关使用 CentOS 安装 tshark 抓包工具 Go学习笔记(四) | win上使用VSCode搭建Go开发环境 日常 Postgres 数据库点滴记录 简单了解 PKCS 规范 美食篇 | DIY戚风蛋糕(烤箱做蛋糕) Go学习笔记(三) | 怎么写Go基准测试(性能测试) TLS1.3正式更新,为Nginx添加TLS1.3的支持 一次诡异的数据库删除 GitHub Pages自定义域名开启HTTPS 证书透明度是什么?它是怎么工作的? Go学习笔记(二) | 我对 recover 的一点误解 搭建证书透明度(certificate-transparency)日志服务之从入门到放弃 修复远程登陆 Centos 时,出现 UTF-8 Warning HTTPS篇之SSL握手过程详解 Go学习笔记(一) | postgres与golang点点滴滴 AWS 命令行界面(aws-cli)从安装到快速上手 数字证书分类及怎么区分各类数字证书 常用 linux 命令小结(一)文件目录操作 云服务器搭建 hexo 博客,git hooks自动更新 SSH 免密登陆, SSH Config 配置 Golang CGO Mac 交叉编译 Windows 使用 goose 让数据库迁移更加轻松
如何用 Go 调用 Windows API
2019-05-22 · via Razeen`s Blog

有时候为了更好的兼容Windows, 或者我们为了获得更高级别功能的访问权限(如配置或创建JobObjects或安全令牌等),我们需要直接去调用Windows的系统API。 很幸运,我们可以利用syscall包与系统直接通信,不用用到CGO 。 然而,也有不方便的地方,如大多数的API,我们需要依赖不安全 (unsafe)的内存管理。

这篇文章,主要记录了我在平时开发过程中以及网上收集到的一些关于Windows API调用相关的知识,或者开发模式,方便你遇到类似的情况后,能更快入手。

1 本文完整Demo在这里。 注2 文章中并未严格区分过程与函数。

关于syscall

在Go中,syscall包会由于你指定的系统或架构的不同而编译出不同的结果,因为syscall包里需要编译的函数或类型会根据你指定的编译参数不同而不同。在导入syscall时你必须在代码中指定"build tags" 或 用指定的文件后缀来命名你的文件。 Dave Cheney 有篇文章深入的介绍了go build机制,可以看一看。简单来说,

  • 如果你的文件命名结构是这样的,name_{GOOS}_{GOARCH}.go 或者 name_{GOOS}.go, 那么这个文件只有在指定的GOOS+指定的GOARCH上才会编译。如:myfile_windows_amd64.go只会在amd64架构CPU的Windows上才会编译。 而myfile_windows.go会在Windows上编译,就不限制CPU架构了。
  • 如果你在go代码顶部增加// +build windows,amd64 注释,那么该文件只会在amd64架构CPU的Windows上才会编译。

关于 unsafe

下面是一段Youtube视频(需要代理)。

视频中 Rob Pike提到:

With the unsafe package there are no guarantees.

是的,Rob Pike不推荐使用unsafe包,因为它没有任何保障

那么,为什么说使用unsafe包没有任何保障呢?

  1. Go (运行时) 不能保证内置类型(如切片、字符串)在不同的Go版本中内存结构是完全一样的。而且作为支持垃圾回收的语言,开发者并不知道Go内存管理的细节。unsafe包会暴露一些内部实现或实际的内存地址,这可能会让你做一些超出预期的事情,如你不小心改变了某个指针指向的地址。

  2. Go (语言层面) 不能保证不同版本之间会有相同的特征或者函数签名,换句话来说,就是在 Go 1.x的兼容性承诺中,并不包含unsafe包。

Warning: Avoid unsafe like the plague; if you can help it.

这两点都告诉我们,在使用unsafe包的时候,我们需要特别的注意应该怎么去使用。我们必须了解,用unsafe包操作内存时,我们能做什么和不能做什么。而且这也可能会因为不同的Go版本而发生变化,在unsafe官方文档中,我们能了解到哪些我们该做,哪些不该做,我们应该密切关注。

Note: 从技术上来说,syscall包,也不在 Go 1.x的兼容性承诺中,因为它也不能保证系统是否向后兼容。不过,从Go1.4开始, go底层基本稳定,只有在操作系统发生变化才会有可能发生更改。而调用Windows DLL的部分改变的可能性比较小,这点对我们是个好消息。

x/sys/windows包中,包含了Go1.x中使用的所有的Windows API调用,你可以直接拿来使用,但注意以下几点:

  1. 该包不在Go 1.x的兼容性承诺中, 不能保证你的代码稳定,如果想保持稳定,可以切到稳定的Git版本中。
  2. 该包的目标也不是暴露所有的Windows API, 而是为Go标准库其他包提供更便携的接口,如os,timenet包。所以你需要的内容,不一定能在该包找到。

虽然是这样,但是现在我们知道了该用那些包去调用Windows API了,同时我们也要知道这会有一定的风险。

Windows API

Microsoft 提供了大部分的Windows API文档。API是通过Windows安装时的DLL(Dynamic Link Library)发布的。DLL是否可用取决于Windows的版本,但API文档中都会列出API什么时候启用,过时或废弃。

加载DLL

要在Go中加载DLL,可以使用syscall.NewLazyDLLsyscall.LoadLibrary

  • NewLazyDLL返回一个*LazyDLL,懒加载,只在第一次调用其函数时才加载库;

  • LoadLibrary是立即加载DLL库。

其实在golang.org/x/sys/windows还支持windows.NewLazySystemDLL的方式加载。这是一种安全的加载方式,它能确保DLL搜索路径被绑定到了Windows系统目录。

创建函数

当我们加载(懒加载)了DLL库过,我们就要使用dll.NewProc("ProcName")去引用一些DLL中的函数(过程)。如:

var
    kernel32DLL = syscall.NewLazyDLL(“kernel32.dll”)
    procOpenProcess = kernel32DLL.NewProc(“OpenProcess”)

一旦有个这些引用,我们就可以Call这个函数本身的方法,或者使用syscall.Syscall函数及其变体进行API调用。使用的过程中发现Call方法更方便,但syscall.Syscall性能更优。根据函数参数的多数,我们可以使用

syscall.Syscall的变体。

  • syscall.Syscall :少于4个参数
  • syscall.Syscall6:4到6个参数
  • syscall.Syscall9:7到9个参数
  • syscall.Syscall12:10到12个参数
  • syscall.Syscall15:13到15个参数

目前Go v1.12中,无法调用超过15个参数的函数。虽然我从来没有遇到过,但在于OpenGL中确实有这种情况。

API函数签名

在实际调用DLL函数之前,我们必须要了解一下过程所需要的参数,类型,大小。Microsoft将此描述为Windows API文档的一部分。如CreateJobObjectA的过程签名如下:

HANDLE CreateJobObjectA(
  LPSECURITY_ATTRIBUTES lpJobAttributes,
  LPCSTR                lpName
);

也就是说,CreateJobObjectA需要一个指向LPSECURITY_ATTRIBUTES结构的指针,和一个指向C-String的指针(ASCII编码,技术上是Windows-1252编码 ;它与ASCII兼容)。

C结构与Go结构

在文档中我们可以搜索到,LPSECURITY_ATTRIBUTES是这么定义的:

typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

这时,我们就必须构造一个类似的Go结构来替代它。这时我们可以参考syscallSecurityAttributes的定义。

在Windows API中,我们可以看到,SecurityAttributes是这么定义的:

typedef struct _SECURITY_ATTRIBUTES {
  DWORD  nLength;
  LPVOID lpSecurityDescriptor;
  BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

而Go中SecurityAttributes的定义为:

type SecurityAttributes struct {
    Length             uint32
    SecurityDescriptor uintptr
    InheritHandle      uint32
}

由此我们大概知道, DWORD对应Go uint32LPVOID (* void)对应uintptrBOOL对应uint32。所以在你不知道用什么类型来表示C中对应的结构时,你可以去看看syscallgo.sys库中找找,或许能有收获。Windows一些参考类型这里也有描述。

然而,了解下面这些常见C类型与Go类型的对应关系会非常有用。

type (
	BOOL          uint32
	BOOLEAN       byte
	BYTE          byte
	DWORD         uint32
	DWORD64       uint64
	HANDLE        uintptr
	HLOCAL        uintptr
	LARGE_INTEGER int64
	LONG          int32
	LPVOID        uintptr
	SIZE_T        uintptr
	UINT          uint32
	ULONG_PTR     uintptr
	ULONGLONG     uint64
	WORD          uint16
)

字符串

在Windows中,一些函数使用的字符串有两种类型:一种是ANSI编码的,一种是UTF-16编码的。

CreateProcess函数。

var (
    kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
    procCreateProcessA = kernel32DLL.NewProc("CreateProcessA")
    procCreateProcessW = kernel32DLL.NewProc("CreateProcessW")
)

不管是哪一种,我们都不能直接使用Go中的字符串。这就需要我们去做一些兼容。其实这很简单,只要我们在原始字符串后面加上一个零值即可。如下:

package main

import "unicode/utf16"

// StringToCharPtr converts a Go string into pointer to a null-terminated cstring.
// This assumes the go string is already ANSI encoded.
func StringToCharPtr(str string) *uint8 {
	chars := append([]byte(str), 0) // null terminated
	return &chars[0]
}

// StringToUTF16Ptr converts a Go string into a pointer to a null-terminated UTF-16 wide string.
// This assumes str is of a UTF-8 compatible encoding so that it can be re-encoded as UTF-16.
func StringToUTF16Ptr(str string) *uint16 {
	wchars := utf16.Encode([]rune(str + "\x00"))	
	return &wchars[0]
}

其中StringToUTF16Ptr在标准库syscall中已经有了。

调用API

把上面这些知识都用到,我们就可以开始调用一些API了。如我们调用CreateJobObjectW

var (
	kernel32DLL          = syscall.NewLazyDLL("kernel32.dll")
	procCreateJobObjectW = kernel32DLL.NewProc("CreateJobObjectW")
)

// CreateJobObject uses the CreateJobObjectW Windows API Call to create and return a Handle to a new JobObject
func CreateJobObject(attr *syscall.SecurityAttributes, name string) (syscall.Handle, error) {
	r1, _, err := procCreateJobObjectW.Call(
		uintptr(unsafe.Pointer(attr)),
		uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(name))),
	)
	if err != syscall.Errno(0) {
		return 0, err
	}
	return syscall.Handle(r1), nil
}

不管调用哪个API,Call的模式都是一样的。

而且syscall.Syscall函数始终返回r1,r2 uintptr,err error, 就最近的实践(windows_amd64)来看,基本可以确定:

  • r1 始终返回 syscall的值;

  • r2 暂且使用;

  • err 返回调用Windows APIGetLastError的结果,这是syscall自动调用的。

而你传入Call中的值必须全部是uintptr,不管你原来的类型是什么。但,Go的指针很特别。

由于Go支持垃圾回收,标准的Go指针不是直接指向了物理内存中的一个地址。Go在运行时可以轻松的修改Go指针指向的物理内存地址,如增加堆栈时。当我们把一个Go指针通过unsafe.Pointer转换成uintptr时,对Go运行时来说,该指针变成了一个未被Go运行时追踪对一个数字而已。即使在下一个指令内,我们也无法确定这个数字指向的是否是它原来指向的那块有效的内存!

正因为如此,我们必须在Syscalls调用时,将指针指向确定的内存。使用uintptr(unsafe.Pointer(&x))构造一个参数,告诉编译器,在Syscall期间不能修改x的内存空间。这样,C函数就能正常的去处理该指针了,直到Syscall返回为止。

godoc for unsafe.Pointer中中写明了四种unsafe.Pointers的操作方式原则。这里用到

(4) Conversion of a Pointer to a uintptr when calling syscall.Syscall.

获取原始数据

有时,Syscall会自动为你填充C结构的内存块,如果你要使用就必须将其转化为可用的类型。

许多API的一般调用模式如下:

  1. 通过空缓冲区调用一次API,指定一个获取缓冲区长度的变量,获取缓冲区的实际大小;

  2. API返回ERROR_INSUFFICIENT_LENGTH错误,同时将长度值更新为实际需要的长度;

  3. 指定一个实际长度的扩展缓冲区,重新调用;

  4. 调用成功。

如,我们需要调用GetExtendedTcpTable函数。

IPHLPAPI_DLL_LINKAGE DWORD GetExtendedTcpTable(
  PVOID           pTcpTable,
  PDWORD          pdwSize,
  BOOL            bOrder,
  ULONG           ulAf,
  TCP_TABLE_CLASS TableClass,
  ULONG           Reserved
);

GetExtendedTcpTable返回的数据为pTcpTablepdwSize , 思路如下:

  1. 我们第一次将pTcpTable直接指一个0值,使用dwSize来获取pTcpTable的实际长度;
  2. 这时,会返回错误ERROR_INSUFFICIENT_BUFFER, 同时dwSize的值被设置成了pTcpTable的实际大小;
  3. 指定一个dwSize大小的[]byte接收数据;
  4. 成功。

部分代码如下:

var (
  iphlpapiDLL             = syscall.NewLazyDLL("iphlpapi.dll")
	procGetExtendedTcpTable = iphlpapiDLL.NewProc("GetExtendedTcpTable")
)

// GetExtendedTcpTable function retrieves a table that contains a list of TCP endpoints available to the application.
func GetExtendedTcpTable(order, ulAf, tableClass uint32) ([]byte, error) {

	var dwSize uint32
	ret, _, err := procGetExtendedTcpTable.Call(
		0,                                // PVOID
		uintptr(unsafe.Pointer(&dwSize)), // PDWORD
		uintptr(order),                   // BOOL
		uintptr(ulAf),                    // ULONG
		uintptr(tableClass),              // TCP_TABLE_CLASS
		0,                                // ULONG
	)
	if ret == 0 {
		return nil, errors.Wrapf(err, "get extended tcp table size failed code %x", ret)
	}

	if syscall.Errno(ret) == syscall.ERROR_INSUFFICIENT_BUFFER {
		buffer := make([]byte, int(dwSize))

		ret, _, err := procGetExtendedTcpTable.Call(
			uintptr(unsafe.Pointer(&buffer[0])),
			uintptr(unsafe.Pointer(&dwSize)),
			uintptr(order),
			uintptr(ulAf),
			uintptr(tableClass),
			uintptr(uint32(0)),
		)

		if ret != 0 {
			return nil, errors.Wrapf(err, "get extended tcp table failed code %x", ret)
		}

		return buffer, nil
	}

	return nil, errors.Wrapf(err, "get extended tcp table size failed code %x", ret)
}

如果你看过上面函数的API,你应该会知道输入参数ulAfTableClass的值 决定了输出的buffer具体的内容。

如果我们输入的是AF_INET + TCP_TABLE_OWNER_PID_ALL 那么我们得到的数据的实际结构应该是MIB_TCPTABLE_OWNER_PID,其结构如下:

typedef struct _MIB_TCPTABLE_OWNER_PID {
  DWORD                dwNumEntries;
  MIB_TCPROW_OWNER_PID table[ANY_SIZE];
} MIB_TCPTABLE_OWNER_PID, *PMIB_TCPTABLE_OWNER_PID;

这里第一个参数dwNumEntries指明有MIB_TCPROW_OWNER_PID table的数量。

而第二个参数则是一个变长的数组。。。 那么我们该怎么用Go去表示呢?

处理变长数据

其实我们可以利用数组来创建一个兼容该结构的Go结构,这要得益于Go中数组的内存布局为连续的内存区域。

我们定义的对应结构如下:

type MIB_TCPTABLE_OWNER_PID struct {
	dwNumEntries uint32
	table        [1]MIB_TCPROW_OWNER_PID
}

type MIB_TCPROW_OWNER_PID struct {
	dwState      uint32
	dwLocalAddr  [4]byte
	dwLocalPort  uint32
	dwRemoteAddr [4]byte
	dwRemotePort uint32
	dwOwningPid  uint32
}

你会说,怎么table的长度只为1,这里先存个疑问。

现在我们首先要知道dwNumEntries的大小,我们才能确定table的数量。于是利用unsafe.Pointer将buffer内的数据转换为Go结构。

	pTable := (*MIB_TCPTABLE_OWNER_PID)(unsafe.Pointer(&buffer[0]))

这里,我们将一个指针指向缓冲区的第一个字节的内存地址,然后利用unsafe.Pointer我们可以将该指针转换为任意类型的指针。其实这个操作是非常危险的,如果你不知道为什么要转换的话。这里我们能转化是因为我们遵循unsafe.Pointer文档中的第一条

(1) Conversion of a *T1 to Pointer to *T2.

Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, this conversion allows reinterpreting data of one type as data of another type.

也就是说,要把*T1转换为*T2,那么T2的长度必须比T1的长,而且T1,T2的内存布局必须相同。

那么,刚刚我们把table 的类型指定为[1]MIB_TCPROW_OWNER_PID, 长度为1显然不是正确的大小。但这没关系,因为[1]MIB_TCPROW_OWNER_PID的长度肯定是小于实际[1+N]MIB_TCPROW_OWNER_PID的长度的。而且他们的内存布局是一样的。

由于此时,我们已经知道了dwNumEntries的大小,我们可以使用unsafe.Pointer的另一规则来遍历获取数组。

(3) Conversion of a Pointer to a uintptr and back, with arithmetic.

	rows := make([]MIB_TCPROW_OWNER_PID, int(pTable.dwNumEntries))
	for i := 0; i < int(pTable.dwNumEntries); i++ {
		rows[i] = *(*MIB_TCPROW_OWNER_PID)(unsafe.Pointer(
			uintptr(unsafe.Pointer(&pTable.table[0])) +
				uintptr(i)*unsafe.Sizeof(pTable.table[0])))
	}

在这里,我们利用规则 (3)迭代已知长度的数组,因为我们知道第一个元素的位置,每个元素的大小,元素的个数,以及结构在内存中的布局是连续的。

这里还有一个更简单的方法,能让我们直接获取table的数据:

	rows2 := ((*[1 << 30]MIB_TCPROW_OWNER_PID)(unsafe.Pointer(&pTable.table[0]))[:int(pTable.dwNumEntries):int(pTable.dwNumEntries)])

这种做法一开始就将这个指针转换成一个非常大的数组指针,然后使用正确的长度取获取实际的内容。好处是不用创建其他切片,复制数据;缺点就是我们需要分配一个足够大的内存去接收,这个大小各平台会有一些差异。

你可以在这里体验一下Go Playground

最后

现在你应该知道了调用Windows API的一些基本步骤与方法,如果遇到问题可以留言,我们一起解决~