[转帖]bpftool perf kprobe uprobe

bpftool,perf,kprobe,uprobe · 浏览次数 : 0

小编点评

**uprobe 事件** uprobe 是一个可执行程序或库的事件通知机制,允许开发者跟踪其执行过程中对内存和 CPU 的访问。它可以用于获取执行程序的执行信息,例如寄存器地址、函数入口地址和内存访问信息。 **注册 uprobe 事件** 可以使用 `set_uprobe_events()` 函数注册 uprobe 事件。参数是一个字符串,表示要监控的事件名称。例如,要监控 `zfree` 函数的退出事件,可以使用以下命令: ```bash set_uprobe_events("zfree_exit") ``` **获取注册的 uprobe 事件** 可以使用 `get_uprobe_events()` 函数获取注册的 uprobe 事件。该函数返回一个字符串数组,其中每个元素表示一个事件的名称。 **示例** 以下示例展示如何注册 `zfree_entry` 和 `zfree_exit` 事件,并获取它们: ```bash # 注册 uprobe 事件 set_uprobe_events("zfree_entry zfree_exit") # 获取注册的事件 events = get_uprobe_events() # 打印注册的事件 for event in "${events[@]}"; do echo "$event" done ``` **输出** 以下是在 `trace` 文件中打印的输出,显示 `zfree` 事件的详细信息: ``` zsh-24842 [006] 258544.995456: zfree_entry: (0x446420) arg1=446420 arg2=79 zsh-24842 [007] 258545.000270: zfree_exit: (0x446540 <- 0x446420) arg1=446540 arg2=0 ``` **注意** * `set_uprobe_events()` 和 `get_uprobe_events()` 函数需要在执行程序之前调用。 * `uprobe` 事件可以与 `kprobe` 事件一起使用,以获取更丰富的信息。 * `uprobe` 是可执行程序和库的标准功能,但某些操作系统可能不支持其。

正文

https://zhuanlan.zhihu.com/p/543218645

 

1、perf子命令用来显示哪些BPF程序正在通过perf_event_open进行挂载。
# bpftool perf (bpf只支持这么一个简单的显示功能,具体bpf程序怎么去通过perf_event_open挂载的需要再研究下)

pid 21711  fd 5: prog_id 5  kprobe  func __x64_sys_write  offset 0
pid 21765  fd 5: prog_id 7  kretprobe  func __x64_sys_nanosleep  offset 0
pid 21767  fd 5: prog_id 8  tracepoint  sys_enter_nanosleep
pid 21800  fd 5: prog_id 9  uprobe  filename /home/yhs/a.out  offset 1159

linux kerbel中bpftool代码在 tools/bpf/bpftool/perf.c中,具体代码流程为:

do_show---->show_proc---->bpf_task_fd_query---->sys_bpf(BPF_TASK_FD_QUERY, &attr, sizeof(attr));

从代码流程可得到bpftool perf可现实的类型有:

raw_tracepoint, tracepoint, kprobe, kretprobe, uprobe, uretprobe

显示的信息方法为遍历/proc目录下的每个pid,然后系统调用sys_bpf查询该pid是否为我们所要找的pid

2、那么perf_event_open是什么?能用来干嘛?

最直接的当然还是查看相关手册,如下。

但下面我还是想试着根据自己的理解稍作总结。

2.1、一段话概括

perf_event_open()返回一个会被随后系统调用( read(2) , mmap(2) , prctl(2) , fcntl(2) ,等)的文件描述符。对perf_event_open()的一次调用创建一个文件描述符,它运行测量性能信息。每个文件描述符与被检测的一个事件相对应,这些可以同时的被分组一起来检测多个事件。事件可以通过两种方法被激活或者关闭:通过 ioctl(2)和prctl(2)。当一个事件被关闭,它不count或者发生溢出,但它还是存在的并且有它的计算值。事件的发生有两个特点(flavors):计算(counting)和采样(sampled)。一个计算事件被用来统计事件发生的集合数。通常来说,计算事件结果通过一个read(2)调用被收集。一个采样事件周期性的写测量值到一个能通过map(2)被访问的buffer。

2.2、举个例子

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/perf_event.h>
#include <asm/unistd.h>
 
#define PID_NUM 123
 
static long
perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                int cpu, int group_fd, unsigned long flags)
{
    int ret;
 
    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                   group_fd, flags);
    return ret;
} 
int
main(int argc, char **argv)
{
    struct perf_event_attr pe;
    long long count,cycles,instructions;
    double ipc;
    int fd;
    int i;
 
    memset(&pe, 0, sizeof(struct perf_event_attr));
    pe.type = PERF_TYPE_HARDWARE; //这个type也可以有很多种。
    pe.size = sizeof(struct perf_event_attr);
    pe.config = PERF_COUNT_HW_CPU_CYCLES;
    pe.disabled = 1;
    pe.exclude_kernel = 1;
    pe.exclude_hv = 1;
 
    //count cycles;
    fd = perf_event_open(&pe, PID_NUM, -1, -1, 0);
    if (fd == -1) {
       fprintf(stderr, "Error opening leader %llx\n", pe.config);
       exit(EXIT_FAILURE);
    }
    ioctl(fd, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    read(fd, &count, sizeof(long long)); //PERF_COUNT_HW_CPU_CYCLES
    cycles=count;
 
    //count instructions
    pe.config = PERF_COUNT_HW_INSTRUCTIONS;
    fd = perf_event_open(&pe, PID_NUM, -1, -1, 0);
    if (fd == -1) {
       fprintf(stderr, "Error opening leader %llx\n", pe.config);
       exit(EXIT_FAILURE);
    }
    ioctl(fd, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    read(fd, &count, sizeof(long long));//PERF_COUNT_HW_INSTRUCTIONS
    instructions=count;
 
    ipc=(double)instructions/(double)cycles;
    printf("Used %lld instructions, %lld cycles ,ips=%f\n", instructions,cycles,ipc);
 
    close(fd);
}

一句话总结:通过调用perf_event_open打开一个文件描述符,然后可对文件描述符进行read等操作,如上例子就是通过read调用获取cpu的cycle数和指令数。

2.3、重要结构体: perf_event_attr

struct perf_event_attr {
               __u32 type;                 /* Type of event */
               __u32 size;                 /* Size of attribute structure */
               __u64 config;               /* Type-specific configuration */
               ......
               ......
               ......
}
重要结构体重比较重要的两个参数为type和config,这两个参数配合使用。
type:
PERF_TYPE_HARDWARE

PERF_TYPE_SOFTWARE
                    
PERF_TYPE_TRACEPOINT
                    
PERF_TYPE_HW_CACHE

PERF_TYPE_RAW
          
PERF_TYPE_BREAKPOINT (since Linux 2.6.33)
                     
dynamic PMU
                     
kprobe and uprobe (since Linux 4.17)
                    
config:
If type is PERF_TYPE_HARDWARE,config can be:

PERF_COUNT_HW_CPU_CYCLES           

PERF_COUNT_HW_INSTRUCTIONS                

PERF_COUNT_HW_CACHE_REFERENCES
                         
PERF_COUNT_HW_CACHE_MISSES                 

PERF_COUNT_HW_BRANCH_INSTRUCTIONS              

PERF_COUNT_HW_BRANCH_MISSES              

PERF_COUNT_HW_BUS_CYCLES
    
PERF_COUNT_HW_STALLED_CYCLES_FRONTEND (since Linux 3.0)          

PERF_COUNT_HW_STALLED_CYCLES_BACKEND (since Linux 3.0)       

PERF_COUNT_HW_REF_CPU_CYCLES (since Linux 3.3)

int syscall(SYS_perf_event_open, struct perf_event_attr *attr,
                   pid_t pid, int cpu, int group_fd, unsigned long flags);
The pid and cpu arguments allow specifying which process and CPU //pid,cpu的设置可以使得监测特定进程和特定cpu上的活动。
       to monitor:

       pid == 0 and cpu == -1
              This measures the calling process/thread on any CPU.

       pid == 0 and cpu >= 0
              This measures the calling process/thread only when running
              on the specified CPU.

       pid > 0 and cpu == -1
              This measures the specified process/thread on any CPU.

       pid > 0 and cpu >= 0
              This measures the specified process/thread only when
              running on the specified CPU.

       pid == -1 and cpu >= 0
              This measures all processes/threads on the specified CPU.
              This requires CAP_PERFMON (since Linux 5.8) or
              CAP_SYS_ADMIN capability or a
              /proc/sys/kernel/perf_event_paranoid value of less than 1.

       pid == -1 and cpu == -1
              This setting is invalid and will return an error.

3、kprobe

kprobe工作原理

3.1、struct kprobe结构体

struct kprobe {
	struct hlist_node hlist;
	/* list of kprobes for multi-handler support */
	struct list_head list;
	/*count the number of times this probe was temporarily disarmed */
	unsigned long nmissed;
	/* location of the probe point */
	kprobe_opcode_t *addr;
	/* Allow user to indicate symbol name of the probe point */
	const char *symbol_name;
	/* Offset into the symbol */
	unsigned int offset;
	/* Called before addr is executed. */
	kprobe_pre_handler_t pre_handler;
	/* Called after addr is executed, unless... */
	kprobe_post_handler_t post_handler;
	/*
	 * ... called if executing addr causes a fault (eg. page fault).
	 * Return 1 if it handled fault, otherwise kernel will see it.
	 */
	kprobe_fault_handler_t fault_handler
	/*
	 * ... called if breakpoint trap occurs in probe handler.
	 * Return 1 if it handled break, otherwise kernel will see it.
	 */
	kprobe_break_handler_t break_handler;
	/* Saved opcode (which has been replaced with breakpoint) */
	kprobe_opcode_t opcode;
	/* copy of the original instruction */
	struct arch_specific_insn ainsn;
	/*
	 * Indicates various status flags.
	 * Protected by kprobe_mutex after this kprobe is registered.
	 */
	u32 flags;
};
其中各个字段的含义如下:
struct hlist_node hlist:被用于kprobe全局hash,索引值为被探测点的地址;
struct list_head list:用于链接同一被探测点的不同探测kprobe;
kprobe_opcode_t *addr:被探测点的地址;
const char *symbol_name:被探测函数的名字;
unsigned int offset:被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;
kprobe_pre_handler_t pre_handler:在被探测点指令执行之前调用的回调函数;
kprobe_post_handler_t post_handler:在被探测指令执行之后调用的回调函数;
kprobe_fault_handler_t fault_handler:在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数;
kprobe_break_handler_t break_handler:在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe;
kprobe_opcode_t opcode:保存的被探测点原始指令;
struct arch_specific_insn ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);
u32 flags:状态标记。
涉及的API函数接口如下:
int register_kprobe(struct kprobe *kp)      //向内核注册kprobe探测点
void unregister_kprobe(struct kprobe *kp)   //卸载kprobe探测点
int register_kprobes(struct kprobe **kps, int num)     //注册探测函数向量,包含多个探测点
void unregister_kprobes(struct kprobe **kps, int num)  //卸载探测函数向量,包含多个探测点
int disable_kprobe(struct kprobe *kp)       //临时暂停指定探测点的探测
int enable_kprobe(struct kprobe *kp)        //恢复指定探测点的探测

3.2、kprobe_example.c分析与演示

/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
	.symbol_name	= "do_fork", //这里用struct kprobe中addr字段也是一样的效果,即用do_fork的函数指针
};
static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;
	ret = register_kprobe(&kp);
	if (ret < 0) {
		printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
	return 0;
}
static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

程序中定义了一个struct kprobe结构实例kp并初始化其中的symbol_name字段为“do_fork”,表明它将要探测do_fork函数。在模块的初始化函数中,注册了 pre_handler、post_handler和fault_handler这3个回调函数分别为handler_pre、handler_post和handler_fault,最后调用register_kprobe注册。在模块的卸载函数中调用unregister_kprobe函数卸载kp探测点。

3.3、kprobe_example.c中回调函数

static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lx\n",
		p->addr, regs->ip, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lx\n",
		p->addr, regs->nip, regs->msr);
#endif
#ifdef CONFIG_MIPS
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, epc = 0x%lx,"
			" status = 0x%lx\n",
		p->addr, regs->cp0_epc, regs->cp0_status);
#endif
#ifdef CONFIG_TILEGX
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, pc = 0x%lx,"
			" ex1 = 0x%lx\n",
		p->addr, regs->pc, regs->ex1);
#endif
	/* A dump_stack() here will give a stack backtrace */
	return 0;
}
handler_pre回调函数的第一个入参是注册的struct kprobe探测实例,第二个参数是保存的触发断点前的寄存器状态,它在do_fork函数被调用之前被调用,该函数仅仅是打印了被探测点的地址,保存的个别寄存器参数。由于受CPU架构影响,这里对不同的架构进行了宏区分(虽然没有实现arm架构的,但是支持的,可以自行添加);
/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
		p->addr, regs->msr);
#endif
#ifdef CONFIG_MIPS
	printk(KERN_INFO "post_handler: p->addr = 0x%p, status = 0x%lx\n",
		p->addr, regs->cp0_status);
#endif
#ifdef CONFIG_TILEGX
	printk(KERN_INFO "post_handler: p->addr = 0x%p, ex1 = 0x%lx\n",
		p->addr, regs->ex1);
#endif
}
handler_post回调函数的前两个入参同handler_pre,第三个参数目前尚未使用,全部为0;该函数在do_fork函数调用之后被调用,这里打印的内容同handler_pre类似。
/*
 * fault_handler: this is called if an exception is generated for any
 * instruction within the pre- or post-handler, or when Kprobes
 * single-steps the probed instruction.
 */
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* Return 0 because we don't handle the fault. */
	return 0;
}

handler_fault回调函数会在执行handler_pre、handler_post或单步执行do_fork时出现错误时调用,这里第三个参数时具体发生错误的trap number,与架构相关,例如i386的page fault为14。

3.4、总结:

kprobe通过动态加载内核模块的方式,能在指定的内核函数进行探测,即可以在指定函数执行前后执行自定义的handler_pre函数和handler_post函数来获取一些我们需要的内核调试信息。

4、uprobe

参考官方文档Uprobe-tracer: Uprobe-based Event Tracing

uprobe简介:

p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a uprobe
r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return uprobe (uretprobe)
p[:[GRP/]EVENT] PATH:OFFSET%return [FETCHARGS] : Set a return uprobe (uretprobe)
-:[GRP/]EVENT                           : Clear uprobe or uretprobe event

GRP           : Group name. If omitted, "uprobes" is the default value.
EVENT         : Event name. If omitted, the event name is generated based
                on PATH+OFFSET.
PATH          : Path to an executable or a library.
OFFSET        : Offset where the probe is inserted.
OFFSET%return : Offset where the return probe is inserted.

FETCHARGS     : Arguments. Each probe can have up to 128 args.
 %REG         : Fetch register REG
 @ADDR        : Fetch memory at ADDR (ADDR should be in userspace)
 @+OFFSET     : Fetch memory at OFFSET (OFFSET from same file as PATH)
 $stackN      : Fetch Nth entry of stack (N >= 0)
 $stack       : Fetch stack address.
 $retval      : Fetch return value.(\*1)
 $comm        : Fetch current task comm.
 +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*2)(\*3)
 \IMM         : Store an immediate value to the argument.
 NAME=FETCHARG     : Set NAME as the argument name of FETCHARG.
 FETCHARG:TYPE     : Set TYPE as the type of FETCHARG. Currently, basic types
                     (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types
                     (x8/x16/x32/x64), "string" and bitfield are supported.

(\*1) only for return probe.
(\*2) this is useful for fetching a field of data structures.
(\*3) Unlike kprobe event, "u" prefix will just be ignored, becuse uprobe
      events can access only user-space memory.

很显然,从上述简介可以得知,uprobe可以探测执行中的用户态程序一些信息,这些信息包括:

1)指定probe(指定偏移地址处)处寄存器信息

2)指定用户态地址的内存

3)指定可执行文件偏移地址处的内存

4)第几次进栈

5)函数返回值

6)获取一片数据结构对于内存中的数据

使用示例:

  • 添加一个探针作为新的 uprobe 事件,将新定义写入 uprobe_events,如下所示(在可执行文件 /bin/bash 中的偏移量 0x4245c0 处设置一个 uprobe)
echo 'p /bin/bash:0x4245c0' > /sys/kernel/debug/tracing/uprobe_events
  • 添加一个探针作为新的 uretprobe 事件:
echo 'r /bin/bash:0x4245c0' > /sys/kernel/debug/tracing/uprobe_events
  • 取消注册事件:
echo '-:p_bash_0x4245c0' >> /sys/kernel/debug/tracing/uprobe_events
  • 打印出注册的事件:
cat /sys/kernel/debug/tracing/uprobe_events

以下示例显示了如何将指令指针和 %ax 寄存器转储到探测的文本地址。探测 /bin/zsh 中的 zfree 函数:

# cd /sys/kernel/debug/tracing/
# cat /proc/`pgrep zsh`/maps | grep /bin/zsh | grep r-xp
00400000-0048a000 r-xp 00000000 08:03 130904 /bin/zsh
# objdump -T /bin/zsh | grep -w zfree
0000000000446420 g    DF .text  0000000000000012  Base        zfree

0x46420 是在 0x00400000 加载的对象 /bin/zsh 中 zfree 的偏移量。因此,uprobe 的命令是:

# echo 'p:zfree_entry /bin/zsh:0x46420 %ip %ax' > uprobe_events

与 uretprobe 相同的是:

# echo 'r:zfree_exit /bin/zsh:0x46420 %ip %ax' >> uprobe_events

我们可以通过查看 uprobe_events 文件来查看注册的事件。

# cat uprobe_events
p:uprobes/zfree_entry /bin/zsh:0x00046420 arg1=%ip arg2=%ax
r:uprobes/zfree_exit /bin/zsh:0x00046420 arg1=%ip arg2=%ax

您可以通过 /sys/kernel/debug/tracing/trace 查看跟踪信息。

# cat trace
# tracer: nop
#
#           TASK-PID    CPU#    TIMESTAMP  FUNCTION
#              | |       |          |         |
             zsh-24842 [006] 258544.995456: zfree_entry: (0x446420) arg1=446420 arg2=79
             zsh-24842 [007] 258545.000270: zfree_exit:  (0x446540 <- 0x446420) arg1=446540 arg2=0
             zsh-24842 [002] 258545.043929: zfree_entry: (0x446420) arg1=446420 arg2=79
             zsh-24842 [004] 258547.046129: zfree_exit:  (0x446540 <- 0x446420) arg1=446540 arg2=0

输出显示我们为 pid 24842 触发了 uprobe,其中 ip 为 0x446420,ax 寄存器的内容为 79。并且 uretprobe 被触发,ip 为 0x446540,对应函数入口为 0x446420。

编辑于 2022-07-21 09:30

与[转帖]bpftool perf kprobe uprobe相似的内容:

[转帖]bpftool perf kprobe uprobe

https://zhuanlan.zhihu.com/p/543218645 1 人赞同了该文章 1、perf子命令用来显示哪些BPF程序正在通过perf_event_open进行挂载。# bpftool perf (bpf只支持这么一个简单的显示功能,具体bpf程序怎么去通过perf_event_

[转帖]

Linux ubuntu20.04 网络配置(图文教程) 因为我是刚装好的最小系统,所以很多东西都没有,在开始配置之前需要做下准备 环境准备 系统:ubuntu20.04网卡:双网卡 网卡一:供连接互联网使用网卡二:供连接内网使用(看情况,如果一张网卡足够,没必要做第二张网卡) 工具: net-to

[转帖]

https://cloud.tencent.com/developer/article/2168105?areaSource=104001.13&traceId=zcVNsKTUApF9rNJSkcCbB 前言 Redis作为高性能的内存数据库,在大数据量的情况下也会遇到性能瓶颈,日常开发中只有时刻

[转帖]ISV 、OSV、 SIG 概念

ISV 、OSV、 SIG 概念 2022-10-14 12:29530原创大杂烩 本文链接:https://www.cndba.cn/dave/article/108699 1. ISV: Independent Software Vendors “独立软件开发商”,特指专门从事软件的开发、生产、

[转帖]Redis 7 参数 修改 说明

2022-06-16 14:491800原创Redis 本文链接:https://www.cndba.cn/dave/article/108066 在之前的博客我们介绍了Redis 7 的安装和配置,如下: Linux 7.8 平台 Redis 7 安装并配置开机自启动 操作手册https://ww

[转帖]HTTPS中间人攻击原理

https://www.zhihu.com/people/bei-ji-85/posts 背景 前一段时间,公司北京地区上线了一个HTTPS防火墙,用来监听HTTPS流量。防火墙上线之前,邮件通知给管理层,我从我老大那里听说这个事情的时候,说这个有风险,然后意外地发现,很多人原来都不知道HTTPS防

[转帖]关于字节序(大小端)的一点想法

https://www.zhihu.com/people/bei-ji-85/posts 今天在一个技术群里有人问起来了,当时有一些讨论(不完全都是我个人的观点),整理一下: 为什么网络字节序(多数情况下)是大端? 早年设备的缓存很小,先接收高字节能快速的判断报文信息:包长度(需要准备多大缓存)、地

[转帖]awk提取某一行某一列的数据

https://www.jianshu.com/p/dbcb7fe2da56 1、提取文件中第1列数据 awk '{print $1}' filename > out.txt 2、提取前2列的文件 awk `{print $1,$2}' filename > out.txt 3、打印完第一列,然后打

[转帖]awk 中 FS的用法

https://www.cnblogs.com/rohens-hbg/p/5510890.html 在openwrt文件 ar71xx.sh中 查询设备类型时,有这么一句, machine=$(awk 'BEGIN{FS="[ \t]+:[ \t]"} /machine/ {print $2}' /

[转帖]Windows Server 2022 简体中文版、英文版下载 (updated Oct 2022)

https://sysin.org/blog/windows-server-2022/ Windows Server 2022 正式版,2022 年 10 月更新,VLSC Posted by sysin on 2022-10-27 Estimated Reading Time 8 Minutes