普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月10日首页

每日一题-统计计算机解锁顺序排列数🟡

2025年12月10日 00:00

给你一个长度为 n 的数组 complexity

在房间里有 n 台 上锁的 计算机,这些计算机的编号为 0 到 n - 1,每台计算机都有一个 唯一 的密码。编号为 i 的计算机的密码复杂度为 complexity[i]

编号为 0 的计算机密码已经 解锁 ,并作为根节点。其他所有计算机必须通过它或其他已经解锁的计算机来解锁,具体规则如下:

  • 可以使用编号为 j 的计算机的密码解锁编号为 i 的计算机,其中 j 是任何小于 i 的整数,且满足 complexity[j] < complexity[i](即 j < i 并且 complexity[j] < complexity[i])。
  • 要解锁编号为 i 的计算机,你需要事先解锁一个编号为 j 的计算机,满足 j < i 并且 complexity[j] < complexity[i]

求共有多少种 [0, 1, 2, ..., (n - 1)] 的排列方式,能够表示从编号为 0 的计算机(唯一初始解锁的计算机)开始解锁所有计算机的有效顺序。

由于答案可能很大,返回结果需要对 109 + 7 取余数。

注意:编号为 0 的计算机的密码已解锁,而 不是 排列中第一个位置的计算机密码已解锁。

排列 是一个数组中所有元素的重新排列。

 

示例 1:

输入: complexity = [1,2,3]

输出: 2

解释:

有效的排列有:

  • [0, 1, 2]
    • 首先使用根密码解锁计算机 0。
    • 使用计算机 0 的密码解锁计算机 1,因为 complexity[0] < complexity[1]
    • 使用计算机 1 的密码解锁计算机 2,因为 complexity[1] < complexity[2]
  • [0, 2, 1]
    • 首先使用根密码解锁计算机 0。
    • 使用计算机 0 的密码解锁计算机 2,因为 complexity[0] < complexity[2]
    • 使用计算机 0 的密码解锁计算机 1,因为 complexity[0] < complexity[1]

示例 2:

输入: complexity = [3,3,3,4,4,4]

输出: 0

解释:

没有任何排列能够解锁所有计算机。

 

提示:

  • 2 <= complexity.length <= 105
  • 1 <= complexity[i] <= 109

别让页面 “鬼畜跳”!Google 钦点的 3 个性能指标,治好了我 80% 的用户投诉

2025年12月9日 23:48

💥告别卡顿!前端性能优化第一课:Google钦点的三大核心指标,你真的懂吗?

欢迎来到前端性能优化专栏的第一课!在这个“用户体验至上”的时代,一个卡顿、缓慢、乱跳的网站,就像一辆抛锚在高速公路上的跑车,再酷炫也只会让人抓狂。别担心,Google已经为你准备好了一份“体检报告”——核心Web指标(Core Web Vitals)

今天,我们就来揭开这份报告的神秘面纱,用最通俗易懂的方式,让你彻底搞懂这三大指标,迈出性能优化的第一步!

✨ LCP(Largest Contentful Paint):最大内容绘制

🚀 衡量:加载性能(你的“第一印象”)

LCP,直译过来是最大内容绘制。它衡量的是用户在访问页面时,视口中最大的那块可见内容(图片或文本块)完成渲染所需的时间

简单来说,它回答了一个核心问题:用户觉得你的页面“加载完成”了吗?

想象一下,你打开一个电商网站,最想看到的是商品大图和价格。LCP就是衡量这个“最重要的东西”多久能出现在你面前。它直接反映了用户对页面加载速度的感知。

指标 衡量维度 优秀标准
LCP 用户感知的加载速度 <= 2.5秒

如果你的 LCP 超过 2.5 秒,用户可能就开始感到不耐烦了。优化 LCP,就是让你的“门面”以最快的速度展示给客人!

LCP 示意图

⚠️ CLS(Cumulative Layout Shift):累积布局偏移

🛡️ 衡量:视觉稳定性(告别“鬼畜”跳动)

CLS,累积布局偏移,听起来有点拗口,但它的作用非常直观:它衡量页面加载过程中元素的非预期移动

你一定遇到过这种情况:正准备点击一个按钮,结果它上面的广告突然加载出来,把按钮挤下去了,你点了个空!这就是布局偏移(Layout Shift)。

CLS 的分数就是用来量化这种“鬼畜”跳动有多严重的。分数越低,代表你的页面布局越稳定,用户体验越丝滑。

指标 衡量维度 理想值
CLS 页面布局稳定性 < 0.1

小贴士: 布局偏移通常是由没有设置尺寸的图片、动态插入的广告或内容导致的。想要 CLS 达标,请给你的元素预留好“坑位”!

⚡️ INP(Interaction to Next Paint):交互到下次绘制

🔄 衡量:整体交互响应性(从“第一印象”到“全程流畅”)

INP,交互到下次绘制,是性能指标家族的新晋“网红”。它衡量的是用户进行点击、触摸或键盘输入后,浏览器需要多长时间才能在屏幕上绘制出视觉反馈。

它取代了老前辈 FID(First Input Delay,首次输入延迟) ,为什么呢?

为什么用 INP 替代 FID? INP 的优势
FID 的局限性 仅测量首次输入延迟,忽略了用户在后续操作中遇到的卡顿。
INP 的全面性 监控用户整个访问周期内的所有交互,更全面。
更真实的用户体验 INP 选取最慢的一次交互作为代表值,反映了“整个使用过程中”的流畅度,而不是仅仅看“第一印象”。

简单来说,FID 就像面试官只看你的简历,而 INP 则是全程跟拍你的工作表现。一个真正流畅的网站,不应该只是第一次点击快,而是从头到尾都快!

INP 与 FID 比较图

🔬 实验室数据 vs. 🌍 现场数据:性能优化的“双重奏”

搞懂了三大指标,接下来我们聊聊如何获取这些数据。性能数据主要分为两大类:实验室数据现场数据

类型 来源(数据渠道) 优点 缺点
实验室数据 (Lab Data) Lighthouse 可控、快速、方便复现问题 非真实用户环境,可能与实际体验有偏差
现场数据 (Field Data) CrUX/Web Vitals 真实用户体验,数据最可靠 不易复现问题,需要时间积累数据

🔧 实验室数据工具

实验室数据就像你在实验室里用精密仪器做的测试,环境是固定的。

  • Lighthouse (Chrome 内置) :Chrome 开发者工具里就能找到,它能快速给你打分,支持 LCP/CLS/INP 评分。
  • PageSpeed Insights:Google 官方工具,它会结合实验室数据(Lighthouse)和现场数据(CrUX),给你一份一站式的性能报告。

🛠️ 现场数据工具

现场数据则是你的网站在真实用户、真实网络、真实设备上跑出来的“实战成绩”。

  • Google Search Console:提供网站整体的核心指标健康报告,是 SEO 优化的重要参考。
  • web-vitals JavaScript 库:这是前端工程师的“秘密武器”。它是一个轻量级的库,可以让你在用户浏览器中实时收集 LCP、CLS、INP 数据,并上报到你的分析后台。

💡 使用 web-vitals 收集性能数据(实战代码)

通过这个库,你可以将真实用户的性能数据发送到你的服务器进行分析,建立自己的性能监测体系(RUM,Real User Monitoring)。

import { onLCP, onCLS, onINP } from 'web-vitals'

function sendToAnalytics(metric) {
  // 将性能指标数据转换为 JSON 字符串
  const body = JSON.stringify(metric)
  
  // 使用 navigator.sendBeacon 或 fetch 发送数据,确保在页面关闭前发送成功
  ;(navigator.sendBeacon && navigator.sendBeacon('/analytics', body)) ||
    fetch('/analytics', { body, method: 'POST', keepalive: true })
}

// 监听并上报三大核心指标
onLCP(sendToAnalytics)
onCLS(sendToAnalytics)
onINP(sendToAnalytics)

✅ 总结:性能优化的“三字真经”

好了,总结一下我们今天学到的前端性能优化的“三字真经”:

  1. LCP:核心内容尽快可见(加载速度要快!)
  2. CLS:页面布局稳定(别再乱跳了!)
  3. INP:交互响应及时(操作要流畅!)

通过 LighthouseSearch Console 以及 Web Vitals 库,我们不仅能建立起一套完善的性能监测体系,还能精准地识别并修复那些让用户抓狂的体验瓶颈。

提升网页质量,不仅能让用户开心,还能让你的网站在 Google 搜索中获得更好的排名。

🤝 了解 CDP (Chrome DevTools Protocol):browser-use 背后的隐藏功臣

作者 卤代烃
2025年12月9日 23:07

Chrome DevTools Protocol (CDP) 是 Chromium 浏览器调试工具的核心通信协议:它基于 JSON 格式,可以通过 WebSocket 实现客户端与浏览器内核之间的双向实时交互。

基于 CDP 的开源产品有许多,其中最有名的应该是 Chrome Devtools FrontendPuppeteerPlaywright 了。

Chrome Devtools Frontend 就是前端开发者天天按 F12 唤起的调试面板,而 Puppeteer 和 Playwright 是非常有名的浏览器自动化操作工具,如今的 agent browser tool(例如 playwright-mcpbrowser-usechrome-devtools-mcp)也是基于它们构建的。可以说每个开发者都在使用 CDP,但因它的定位比较底层,大家常常又意识不到他的存在。

Chrome Devtools Frontend Puppeteer

CDP 有自己的官方文档站和相关的 Github 地址,秉承了 Google 开源项目的一贯风格,简洁,克制,就是没什么可读性。文档和项目都是根据源码变动自动生成的,所以只能用来做 API 的查询,这也导致如果没有相关的领域知识,直接阅读相关文档或 deepwiki 是拿不到什么有建设性内容的。

上面的那些吐槽,也是我写本文的原因,互联网上介绍 CDP 的博文太少了,也没什么系统性的架构分析,所以不如我自己来写丰富一下 AI 的语料库(bushi)。

协议格式

首先 CDP 协议是一个典型的 CS 架构,这里我们拿 Chrome Devtools 为例:

  • Chrome Devtools:就是 Client,用来做调试数据的 UI 展示,方便用户阅读
  • CDP:就是连接 Client-Server 的 Protocol,定义 API 的各种格式和细节
  • Chromium/Chrome:就是 Server,用来产生各种数据

CDP 协议的格式基于 JSON-RPC 2.0 做了一些轻量的定制。首先是去掉了 JSON 结构体中的 "jsonrpc": "2.0" 这种每次都要发送的冗余信息。可以看下面几个 CDP 的实际例子:

首先是常规的 JSON RFC Request/Response,细节不用关注,就看整体的格式:

Target.setDiscoverTargets

// Client -> Chromium
{
  "id":2
  "method": "Target.setDiscoverTargets",
  "params": {"discover":true,"filter":[{}]},
}

// Chromium -> Client
{
  "id": 2,
  "result": {}
}

可以看到这就是一个经典的 JSON RFC 调用,用 id 串起 request 和 response 的关系,然后 request 中通过 methodparams 把请求方法和请求参数带上;response 通过 result 带上响应结果。

关于 JSON RFC Notification(Event)的例子如下,定义也很清晰,就不展开了:

Target.targetCreated

{
  "method": "Target.targetCreated",
  "params": {
    "targetInfo": {
      "targetId": "12345",
      "type": "browser",
      "title": "",
      "url": "",
      "attached": true,
      "canAccessOpener": false
    }
  }
}

众所周知,JSON RFC 只是一套协议标准,它其实可以跑在任意的支持双向通讯的通信协议上。目前 CDP 的主流方案还是跑在 WebSocket 上(也可以用本地 pipe 的方式连接,但用的人少),所以用户可以借助任意的 Websocket 开源库搭建出合适的产品。


Domain 整体分类

如果直接看 CDP 的文档,会发现它的目录侧边栏只有一列,那就是 Domains,然后下面有一堆看起来很熟悉的名词:DOM,CSS,Console,Debugger 等等...

CDP Domains Chrome Devtools Frontend

其实这些 Domain 都可以和 Chrome Devtools 联系起来的。所以我们可以从 Chrome Devtools 的各种功能反推 CDP 中的各种 Domain 作用:

  • Elements:会用到 DOM,CSS 等 domain 的 API
  • Console:会用到 Log,Runtime 等 domain 的 API
  • Network:会用到 Network 等 domain 的 API
  • Performance:会用到 Performance,Emulation 等 domain 的 API
  • ......

那么到这里就有一个比较直观的认识了。我们再返回看 CDP 本身,CDP 其实可以分为两大类,然后下面有不同的 Domain 分类:

  • Browser Protocol:浏览器相关的协议,之下的 Domain 都是平台相关的,比如说 Page,DOM,CSS,Network,都是和浏览器功能相关
  • JavaScript Protocol:JS 引擎相关的协议,主要围绕 JS 引擎功能本身,比如说 Runtime,Debugger,HeapProfiler 等,都是比较纯粹的 JS 语言调试功能

deepwiki 给出的 CDP Domains 分类

了解了 Domain 的整体分类,下一步我们探索一下 Domain 内部的运行流程。

Domain 内部通信

理解某个 Domain 的运行流程,还是老办法,对照着 Chrome Devtools Frontend 的某个调试面板反推,这样理解起来是最快的。

这里我们拿 Console 面板为例,这个基本上是 Web 开发者日常使用频率最高的功能了。

从 UI 面板上看有很多功能,有筛选,分类,分组等各种高级功能,但绝大部分的功能都是前端上的实现,联系到背后和 Console 相关的 CDP 协议,其实主要就 5 条:


举一个真实的例子,我们在 Console 面板先发起一个不合规的网络请求,然后再 log 一句话:

  • 首先每个页面打开 Devtools 的时候,会默认调用 Log.enable 启动 log 监听
  • 手动 fetch 一个不合规的地址时,浏览器会先做安全检查,通过 Log.entryAdded 提示不合规
  • 发起一个真实的网络请求,失败后会通过 Runtime.exceptionThrown 提示 Failed to fetch
  • 最后手动调用 console API,CDP 会发一个 Runtime.consoleAPICalled 的调用 log event

把上面的的例子抽象一下,其实所有的 Domain 的调用流程基本都是一样的:

  • 通过 Domain.enable 开启某个 Domain 的调试功能
  • 开启功能后,就可以在这个阶段发送相关的 methods 调用,也可以监听 Chrome 发来的各种 event
  • 通过 Domain.disable 关闭这个 Domain 的调试功能

部分 Domain 并没有 enable/disable 这两个 methods,具体情况具体分析

Target: 特殊的 Domain

上面介绍了 Domain 的分类和 Domain 内部运转的整体流程,但是有一个 Domain 非常的特殊,那就是 Target

type 分类

Target 是一个较为抽象的概述,它指的是浏览器中的可交互实体

  • 我创建了一个浏览器,那么它本身就是一个 type 为「browser」的 Target
  • 浏览器里有一个标签页,那么这个页面本身就是一个 type 为「page」的 Target
  • 这个页面里要做一些耗时计算创建了一个 Worker,那么它就是一个 type 为「worker」的 Target

目前从 chromium 源码上可以看出,Target 的 type 有以下几种:

  • browser,browser_ui,webview
  • tab,page,iframe
  • worker,shared_worker,service_worker
  • worklet,shared_storage_worklet,auction_worklet
  • assistive_technology,other

从上面的 target type 可以看出,Target 整体是属于一个 scope 比较大的实体,基本上是以进程/线程作为隔离单位分割的,每个 type 下可能包含多个 CDP domain,比如说 page 下就有 Runtime,Network,Storage,Log 等 domain,其他类型同理。

交互流程

Target 的内部分类清晰了,那么还剩重要的一环:如何和 Target 做交互

CDP 这里的逻辑是,先发个请求,向 Target 发起交互申请,然后 Target 就会给你一个 sessionId,之后的交互就在这个 session 信道上进行。CDP 在这里也对 JSON-RPC 2.0 做了一个轻量定制,它们把 sessionId 放在了 JSON 的最外层,和 id 同一个层级:

{
  method: "SystemInfo.getInfo",
  id: 9,
  sessionId: "62584FD718EC0B52B47067AE1F922DF1"
}

我举个实际的例子看 session 的交互流程。

假设我们想从 browser Target 上获取一些系统消息,先假设我们事先已经知道了 browser 的 targetId,那么一个完整的 session 通信如下:

这里为了聚焦 session 的核心交互逻辑,下面的 CDP message 删除了不必要的信息

  1. Client 通过 Target.attachToTarget API 向 browser 发起会话请求,拿到 sessionId
// Client —> Chromium
{
  "method": "Target.attachToTarget",
  "params": {
    "targetId": "31a082d2-ba00-4d8f-b807-9d63522a6112", // browser targetId
    "flatten": true // 使用 flatten 模式,后续将会把 sessionId 和 id 放在同一层级
  },
  "id": 8
}

// Chromium —> Client
{
  "id":8,
  "result": {
    "sessionId": "62584FD718EC0B52B47067AE1F922DF1" // 拿到这次对话的 sessionId
  }
}
  1. Client 带上上一步给的 sessionId,发送一条获取系统信息的 CDP 调用并获取到相关消息
// Client —> Chromium
{
  "method": "SystemInfo.getInfo", // 获取系统信息的方法
  "id": 9,
  "sessionId": "62584FD718EC0B52B47067AE1F922DF1" // sessionId 和 id 同级,在最外层
}

// Chromium —> Client
{
  "id": 9,
  "sessionId": "62584FD718EC0B52B47067AE1F922DF1"
  "result": { /* ... */ },
}
  1. 不想在这个 session 上聊了,调用 Target.detachFromTarget 直接断开连接,自此这个会话就算销毁了
// Client —> Chromium
{
  "method": "Target.detachFromTarget",
  "id": 11,
  "sessionId":"62584FD718EC0B52B47067AE1F922DF1"
}

// Chromium —> Client
{
  "id": 11,
  "result": {}
}

上面的流程可以用下面的图表示:

当然涉及 Target 生命周期的相关 Methods 和 Event 还有很多,一一讲解也不现实,感兴趣的同学可以自己探索。

一对多

除了上述的特性,Target 还有一个特点,那就是一个 Target 允许多个 session 连接。这意味着可以有多个 Client 去控制同一个 Target。这在现实中也是很常见的。比如说对于一个网页实体,它既可以被 Chrome Devtools(Client1)调试,也可以同时被 puppeteer(Client2)连接做自动化控制。当然这也会带来了一些资源访问的并发问题,在实际应用场景上需要万分的小心。

综合案例

综上所述,我们可以看一个实际的例子,把上面的内容都囊括起来。

下面的案例我是用 puppeteer 创建了一个 url 为 about:blank 的新网页时,底层的 CDP 调用流程。调用的源文件可以访问右边的超链接下载:create_about_blank_page.har,har 文件可用 Chrome Devtools Network 导入查看:


首先是最开始的 Target 创建流程。注意下图红框和红线里的内容:

  • 首先调用 Target.createTarget 创建一个 page(在调用 createTarget 时,会同步生成一个 tab Target,我们可以忽略这个行为,不影响后续理解)
  • page Target 创建好后,在响应 Target.createTarget methods 的同时,还会发送一个 Target.targetCreated 的 event,里面有这个 page Target 的详细 meta info,例如 targetId,url,title 等
  • page Target 的 meta info 变动时,会下发 Target.targetInfoChanged event 同步信息变化
  • page Target 下发一个 Target.attachedToTarget 的 event,告知 client 这次连接的 sessionId,这样后续的一些 domain 操作就可以带上 sessionId 保证信道了

Target 创建好后,就要开启这个 page 下的各个 Domain 了:

  • Network.enable:开启 Network Domain 的监听,比如说各种网络请求的 request/response 的细节
  • Page.enable:开启 Page Domain 的监听,比如说 navigation 行为的操纵
  • Runtime.enable:开启 Runtime Domain 的监听,比如说要在 page 里 evaluate 一段注入函数
  • Performance.enable:开启 Performance Domain 的监听,比如说一些 metrics 信息
  • Log.enable:开启 log 相关信息的监听,比如说各种 console.log 信息

开启相关 Domain 后,就可以监听这个 page Target 的相关 event 或者主动触发一些方法,如下图所示:

  • 我们主动执行 Page.getNavigationHistory methods,获取当前页面的 history 导航记录
  • 我们监听到 Runtime.consoleAPICalled event 的触发,拿到了一些 console 信息

相关的细节还有很多就不一一列举了,感兴趣的同学可以看上面的 har 源文件,我相信全部看完后就会对 CDP 有个清晰的认知了。

编码建议

就目前(2025.12)而言,Code Agent 和 DeepResearch 等常见的 AI 辅助编程工具在 CDP 领域上表现并不是很好,主要的原因有 3 点:

  • 预训练语料少:从前文可知,CDP 因为协议过于底层,所以相关的使用案例和代码非常少,模型预训练时语料很少,导致幻觉还是比较严重的
  • 文档质量一般:CDP 文档写的太简洁了,基本就是根据出入参自动生成的类型文档,只能用来查询核实一下,想从中获得完整的概念,对 AI 和人来说还是太难了
  • API 动态迭代:CDP 虽然开源出来了,但其本质还是一个为 Chromium 服务的私有协议,其 latest 版本一直在动态迭代中,所以这种动态变化也影响了 AI 的发挥

综合以上原因,我的一个策略是「小步快跑,随时验证」。方案就是对于自己想实现的功能,先让 AI 出一个大致的方案,但是不要直接在自己的迭代的项目里直接写,而是先生成一个可以快速验证相关功能的最小 DEMO,然后亲自去验证这个方案是否符合预期。

「亲自验证 DEMO 可行性」 这一步非常重要,因为 AI 直出的 CDP 解决方案可靠性并不高,不像 AI -> UI 有较高的容错率和置信度,只有在 DEMO 上验证成功的方案才有迁移到正式项目的价值。

另一个解决方案,就是 puppeteer 等优秀项目上吸取经验。puppeteer 底层也是调用 CDP,而且它迭代了十余年,对一些常见案例已经沉淀了一套成熟的解决方案。通过学习它内部的 CDP 调用流程,可以学习到很多文档未曾描述的运用场景。下一篇 Blog,我们就分析一下 puppeteer 的源码架构,让我们在调用过程中更加得心应手。

如何设置你的 PWN 环境

2025年12月9日 22:36

你首先需要的是 Linux Shell ,因为大多数 pwn 挑战通常是 64 位 ELF 可执行文件 。我会写两个不同的部分,分别针对 Windows 和 MacOS。如果你已经用过任何 Linux 发行版,这部分你就没问题了!对于剩下的 Windows/MacOS 用户,我强烈建议安装 Debian Bookworm 作为首选发行版,因为它目前使用 glibc 2.36,尚未实现完整的 RELRO,  且利用起来稍微容易一些。

Windows

对于正在阅读本文的 Windows 用户,你可以按照链接的说明书轻松下载 WSL。如果你懒得看,这里有一份你应该执行的命令列表。

安装使用 Debian for WSL

wsl.exe --update
wsl --set-default-version 2
wsl install -d Debian

就是这样!从现在起,你可以通过开始菜单中的 Debian 搜索 Debian,或者在命令行中运行 Debian,进入你的 Debian shell。关于上述 WSL 2 的要求,WSL 2 允许你在 WSL 内部的集成桌面模式下使用 GUI 应用。这其实没什么特别的意义,除了你可以让你的调试器在一个新窗口里弹出,不用 tmux,这对我来说简直是个大胜利!!

补充一点,你可能需要更新系统包 ,并下载文本编辑器,以避免在 WSL 终端和其他窗口之间切换。你可以用以下命令完成: sudo apt update && sudo apt dist-upgrade && sudo apt(-get) install <editor of choice> 。

如果你想要完全懒散的体验和最少的 Linux shell 作,你可以挂载你的 Windows 文件系统 ,并在 Downloads 目录中使用以下命令(不区分大小写!): cd /mnt/c/users/your\ name/downloads

macOS

不幸的是,对于 MacOS 用户来说,我觉得没有像 Windows 用户那样的集成桌面。所以,我建议你按照这个指南作,尽可能多地在终端里作 ,你会在其中花费比预期更多的时间。好消息是,有 Lima,我们可以用它帮助简化创建 x64 Linux 壳,即使是在 Apple Silicon 上。

这里有一份快速入门的推荐清单。

brew install lima
limactl start --name=x64-debian template://debian --arch=x86_64
limactl shell x64-debian 
sudo apt install tmux

遇到终端颜色的问题,解决方案完全取决于你的终端模拟器 ,所以你自己找解决方案。

额外加分:可写主机文件系统!

LiMactl 编辑 x64-debian 以编辑配置 添加以下不安全的配置:

mounts:
- location: "~"
  writable: true

现在你拥有了一个运行在 x64 上的 Linux Shell ,可以不受限制地访问你的主机文件系统! 你可以用标签自动补全你的虚拟机名字拼写!

恭喜你通过了 Linux 安装!繁琐的部分现在开始了......我们先着手安装调试器和基础工具。

工具安装


恭喜你完成了Linux安装!乏味的部分现在开始了...让我们先安装调试器和基本工具。

实用工具


pwntools
  1. sudo apt install python3 安装python
  2. sudo apt-get install python3-pwntools 安装pwntools
  3. pwn checksec /lib/x86_64-linux-gnu/libc.so.6 验证它是否工作
one gadget
  1. sudo apt install rubygems 安装gem
  2. gem install one_gadget 安装one gadget
  3. one_gadget /lib/x86_64-linux-gnu/libc.so.6 验证它是否工作
rop gadget
  1. sudo -H python3 -m pip install ROPgadget 安装ROPgadget
  2. ROPgadget /lib/x86_64-linux-gnu/libc.so.6 验证它是否工作(警告:输出量很大!!)

调试器


强烈推荐bata-gef,你也应该使用它!

我不建议盲目运行安装脚本。相反,克隆仓库并在你的.gdbinit中source它的gef.py

cd ~
git clone https://github.com/bata24/gef
vi ~/.gdbinit

#.gdbinit
source /path/to/gef.py

对于没有集成桌面模式的MacOS用户,使用我们之前安装的tmux。只需运行以下命令:

  1. tmux 启动tmux
  2. [Ctrl] + B 进入命令模式
  3. ] 启用滚动
  4. [Ctrl] + C 重置状态
  5. [Ctrl] + B,然后<arrow> 切换焦点标签页

反编译器/反汇编器


太好了!现在我们有了开始编写和调试_pwn_脚本所需的一切,这技术上就是我们需要的全部,但有时,挑战不提供源代码。在这种情况下,需要进行一些逆向工程工作。让我们下载**Ghidra**。

设置相当简单,你只需要一个Java运行时,我推荐Temurin,你可以在

这里 找到它。

一旦你安装了运行时,只需运行ghidraRunghidraRun.bat来启动Ghidra

使用Ghidra

Ghidra实际上相当不错且用户友好,你所要做的就是创建一个空项目,然后拖放你的挑战二进制文件到其中,并给它一点时间来生成反汇编和反编译!

容器化和服务器复制


恭喜你走到这一步!你已经准备好处理几乎所有pwn挑战了。然而,我们可以做得更好。

有时,善意的挑战作者会提供libc、ld和/或Dockerfile。这些文件特别有用,因为它们帮助你确保你的挑战使用与服务器相同的库,确保你永远不会遇到_"在我的机器上可以运行"_的问题!所以,我将介绍如何处理这些文件的各种组合。

给定了Libc、ld和Dockerfile


这是最好的情况,因为你不需要做太多。这里有一个你可以运行的命令来使用给定的libcld

./ld-linux-x86-64.so.2 --library-path . ./chal

这将运行链接器,并告诉它使用你目录中的库来启动挑战。就这样! 你现在正在使用给定的库。但是,我们如何复制服务器呢?

首先,我们必须安装Docker。过程相当长,但请相信

文档 并遵循它。

一旦你安装了Docker,你所要做的就是运行以下命令:

docker run -p YOUR_PORT:CONTAINER_PORT -d --privileged $(docker build -q .)

这将构建容器,然后以特权运行它,这通常是_pwn.red/jail_模板所需要的。请将CONTAINER_PORT占位符替换为容器提供挑战的端口,将YOUR_PORT替换为宿主机上的任何未使用端口。默认情况下,这将绑定到所有接口

设置完成后,你可以像这样连接到容器并运行挑战:

nc 127.0.0.1 YOUR_PORT

就这样!你已经成功复制了服务器使用的库,以及服务器本身!完成后,你应该通过运行docker ps找到容器哈希,然后运行docker stop <hash>来清理正在运行的容器。

只提供了Dockerfile


有时,作者只提供Dockerfile。这很糟糕,但没关系,因为我们可以自己提取libcld。我们首先使用上述步骤启动容器,然后执行以下操作:

docker cp 1a2b:/lib/x86_64-linux-gnu/libc.so.6 ./libc.so.6
docker cp 1a2b:/lib/x86_64-linux-gnu/ld-linux-x86-64 ./ld-linux-x86-64.so.2

记得将1a2b替换为容器哈希的前4个字符,启动时会显示给你。有时,Dockerfile会从另一个Linux发行版复制文件到一个文件夹来提供服务,以便使用该发行版的libc。在这种情况下,只需在路径前加上前缀。使用一个容器执行COPY --from=debian:bookworm / /srv的例子,我们只需在/lib前加上/srv来从服务器的文件中复制。

结论


通过以上所有设置和安装,你应该拥有所有工具,以及开始处理pwn挑战的基本知识。快乐pwning! 如果你现在想了解更多关于pwn的一般知识,请阅读我的博客文章

3577. 统计计算机解锁顺序排列数

作者 stormsunshine
2025年6月9日 06:15

解法

思路和算法

使用已解锁的计算机密码将未解锁的计算机解锁的条件是:已解锁的计算机的编号和密码复杂度分别小于未解锁的计算机的编号和密码复杂度。由于初始时只有编号为 $0$ 的计算机密码已解锁,编号 $0$ 是最小编号,因此对于其他任意编号 $i$,是否可以使用编号为 $0$ 的计算机密码解锁编号为 $i$ 的计算机的情况如下。

  • 当 $\textit{complexity}[i] > \textit{complexity}[0]$ 时,可以使用编号为 $0$ 的计算机密码解锁编号为 $i$ 的计算机。

  • 当 $\textit{complexity}[i] \le \textit{complexity}[0]$ 时,不能使用编号为 $0$ 的计算机密码解锁编号为 $i$ 的计算机。

如果存在 $1 \le i < n$ 的整数 $i$ 满足 $\textit{complexity}[i] \le \textit{complexity}[0]$,则对于任意可以被编号为 $0$ 的计算机密码解锁的计算机的编号 $j$,必有 $\textit{complexity}[j] > \textit{complexity}[0]$,因此 $\textit{complexity}[j] > \textit{complexity}[i]$,即编号为 $j$ 的计算机密码不能解锁编号为 $i$ 的计算机。因此,编号为 $i$ 的计算机无法被解锁,此时无法解锁所有计算机。

如果 $1 \le i < n$ 的所有整数 $i$ 都满足 $\textit{complexity}[i] > \textit{complexity}[0]$,则所有计算机都能被编号为 $0$ 的计算机密码解锁。由于初始时编号为 $0$ 的计算机密码已解锁,因此其余 $n - 1$ 台计算机可以按任意顺序解锁,排列数是 $(n - 1)!$。

代码

###Java

class Solution {
    static final int MODULO = 1000000007;

    public int countPermutations(int[] complexity) {
        long permutations = 1;
        int n = complexity.length;
        for (int i = 1; i < n; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            permutations = permutations * i % MODULO;
        }
        return (int) permutations;
    }
}

###C#

public class Solution {
    const int MODULO = 1000000007;

    public int CountPermutations(int[] complexity) {
        long permutations = 1;
        int n = complexity.Length;
        for (int i = 1; i < n; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            permutations = permutations * i % MODULO;
        }
        return (int) permutations;
    }
}

###C++

const int MODULO = 1000000007;

class Solution {
public:
    int countPermutations(vector<int>& complexity) {
        long long permutations = 1;
        int n = complexity.size();
        for (int i = 1; i < n; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            permutations = permutations * i % MODULO;
        }
        return permutations;
    }
};

###Python

MODULO = 1000000007

class Solution:
    def countPermutations(self, complexity: List[int]) -> int:
        permutations = 1
        n = len(complexity)
        for i in range(1, n):
            if complexity[i] <= complexity[0]:
                return 0
            permutations = permutations * i % MODULO
        return permutations

###C

const int MODULO = 1000000007;

int countPermutations(int* complexity, int complexitySize) {
    long long permutations = 1;
    for (int i = 1; i < complexitySize; i++) {
        if (complexity[i] <= complexity[0]) {
            return 0;
        }
        permutations = permutations * i % MODULO;
    }
    return permutations;
}

###Go

const MODULO = 1000000007

func countPermutations(complexity []int) int {
    permutations := 1
    n := len(complexity)
    for i := 1; i < n; i++ {
        if complexity[i] <= complexity[0] {
            return 0
        }
        permutations = permutations * i % MODULO
    }
    return permutations
}

###JavaScript

const MODULO = 1000000007;

var countPermutations = function(complexity) {
    let permutations = 1;
    let n = complexity.length;
    for (let i = 1; i < n; i++) {
        if (complexity[i] <= complexity[0]) {
            return 0;
        }
        permutations = permutations * i % MODULO;
    }
    return permutations;
};

###TypeScript

const MODULO = 1000000007;

function countPermutations(complexity: number[]): number {
    let permutations = 1;
    let n = complexity.length;
    for (let i = 1; i < n; i++) {
        if (complexity[i] <= complexity[0]) {
            return 0;
        }
        permutations = permutations * i % MODULO;
    }
    return permutations;
};

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{complexity}$ 的长度。需要遍历数组一次。

  • 空间复杂度:$O(1)$。

脑筋急转弯(Python/Java/C++/Go)

作者 endlesscheng
2025年6月8日 12:06

题目说:用计算机 $j$ 解锁计算机 $i$ 的前提是 $j<i$ 且 $\textit{complexity}[j] < \textit{complexity}[i]$。

观察

  • 一开始就解锁的只有计算机 $0$。
  • 第一轮,被 $0$ 解锁的计算机(记作集合 $A$),密码复杂度比 $\textit{complexity}[0]$ 大。
  • 第二轮,被集合 $A$ 中的计算机解锁的计算机(记作集合 $B$),密码复杂度更大,所以也比 $\textit{complexity}[0]$ 大。
  • 第三轮,被集合 $B$ 中的计算机解锁的计算机(记作集合 $C$),密码复杂度更大,所以也比 $\textit{complexity}[0]$ 大。
  • 依此类推,所有被解锁的计算机的密码复杂度都要比 $\textit{complexity}[0]$ 大。

定理:当且仅当计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大,才能解锁所有计算机。

充分性:如果计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大,根据题意,仅用计算机 $0$ 便可解锁所有计算机。

必要性:如果可以解锁所有的计算机,那么计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大。考虑其逆否命题,即如果在计算机 $0$ 的右边存在计算机 $A$,满足 $\textit{complexity}[i] \le \textit{complexity}[0]$,那么不可能解锁计算机 $A$,更不可能解锁所有计算机。为了解锁计算机 $A$,我们需要在其左边找比 $\textit{complexity}[i]$ 更小的计算机。不断往左找,计算机的密码复杂度只会更小,直到找到一台被计算机 $0$ 解锁的计算机 $B$。$B$ 的密码复杂度必须比 $\textit{complexity}[0]$ 大,但为了能解锁计算机 $A$,$B$ 的密码复杂度又要 $< \textit{complexity}[i] \le \textit{complexity}[0]$,矛盾,所以不可能解锁计算机 $A$。

根据定理,如果计算机 $0$ 右边的所有计算机的密码复杂度都比 $\textit{complexity}[0]$ 大,那么我们可以按照任意顺序解锁这 $n-1$ 台计算机,方案数为 $n-1$ 个不同物品的全排列个数,即

$$
(n-1)!
$$

注意取模。关于模运算的知识点,见 模运算的世界:当加减乘除遇上取模

###py

class Solution:
    def countPermutations(self, complexity: List[int]) -> int:
        MOD = 1_000_000_007
        ans = 1
        for i in range(1, len(complexity)):
            if complexity[i] <= complexity[0]:
                return 0
            ans = ans * i % MOD
        return ans

###java

class Solution {
    public int countPermutations(int[] complexity) {
        final int MOD = 1_000_000_007;
        long ans = 1;
        for (int i = 1; i < complexity.length; i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            ans = ans * i % MOD;
        }
        return (int) ans;
    }
}

###cpp

class Solution {
public:
    int countPermutations(vector<int>& complexity) {
        const int MOD = 1'000'000'007;
        long long ans = 1;
        for (int i = 1; i < complexity.size(); i++) {
            if (complexity[i] <= complexity[0]) {
                return 0;
            }
            ans = ans * i % MOD;
        }
        return ans;
    }
};

###go

func countPermutations(complexity []int) int {
const mod = 1_000_000_007
ans := 1
for i := 1; i < len(complexity); i++ {
if complexity[i] <= complexity[0] {
return 0
}
ans = ans * i % mod
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{complexity}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

更多相似题目,见下面思维题单的「§5.2 脑筋急转弯」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

分类讨论

作者 tsreaper
2025年6月8日 12:05

解法:分类讨论

因为只能用小 complexity 解锁大 complexity,所以后续解锁的电脑一定满足 complexity[i] > complexity[0]

换句话说,如果存在 i > 0,使得 complexity[i] <= complexity[0],那就无法解锁该电脑,答案为 $0$。

否则,所有电脑都能通过电脑 $0$ 解锁,因此后续编号可以任意排列,答案为 $(n - 1)!$。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

class Solution {
public:
    int countPermutations(vector<int>& complexity) {
        int n = complexity.size();

        // 检查是否有无法解锁的电脑
        for (int i = 1; i < n; i++) if (complexity[i] <= complexity[0]) return 0;

        // 计算 (n - 1)!
        const int MOD = 1e9 + 7;
        long long ans = 1;
        for (int i = 1; i < n; i++) ans = ans * i % MOD;
        return ans;
    }
};
昨天 — 2025年12月9日首页

鸿蒙智行首款 MPV 命名为「智界 V9」,余承东:超越市面上所有旗舰

作者 芥末
2025年12月9日 22:00

在上个月底的享界 S9 发布会尾声,余承东带来了一个「One more thing」——鸿蒙智行首款旗舰 MPV 将落地智界。

而在刚刚「鸿蒙智行 超凡一步」的直播中,余承东和奇瑞董事长尹同跃宣布这款 MPV 正式被命名为智界 V9。

这也意味着,鸿蒙智行的产品线将将进一步拓展,覆盖轿车、SUV、旅行车、MPV 等多个细分品类。

「超越所有旗舰」

根据直播中和近期谍照中透露的信息,智界 V9 在鸿蒙智行体系中属于旗舰级定位。

智界 V9 车长约 5.3 米,轴距或接近 3.2 米,与岚图梦想家(车长 5315mm)处于同一量级。车身造型整体偏方正,且采用了双电动侧滑门的设计,空间表现和使用便利性应当有保障。

在具体的座舱配置上,直播中透露智界 V9 将配备主副驾双零重力座椅、后排双抽屉、电吸电弹前备箱三项亮点配置。

而从前段时间的谍照中还可以观察到智界 V9 采用了一块横贯主副驾的大连屏,整合仪表、中控与副驾娱乐功能。相比起问界 M9 屏幕,V9 的这块屏幕似乎一体化程度更强,边框更窄,或许采用了华为自研的车规级 OLED 面板。

新车的中控台则是极简风格,配备电子怀挡、双无线充电面板和优化储物格。二排座椅则配备了多向电动调节、加热、通风功能,扶手后方疑似设置了一块触控屏来控制空调与影音。门板内侧则设计了一个旋钮,猜测智界 V9 的二排座椅或许可以支持 360 度旋转,与第三排形成对坐布局。此外,车顶横梁预留位被猜测用于安装后排屏幕。

从车内配置来看,智界 V9 的目标客户似乎并不局限于家庭用户,而是更多希望拓展到商务、聚会等更广泛的出行群体。

动力方面,智界 V9 已经确定将提供增程和纯电两种版本,增程器可能沿用与智界 R7 一致的 1.5T 四缸发动机,纯电版本的 CLTC 续航预计超 600 公里。

而在智能化的部分,智界 V9 将使用一枚 192 线的激光雷达,毫无疑问将搭载乾崑智驾 ADS 4 和鸿蒙座舱 5。

再一次破局

对于智界而言,V9 或许是其冲击高端市场的又一次宝贵机会。

目前智界有两款在售车型,轿车 S7 与中大型 SUV R7。

其中 S7 自上市以来始终未能站稳脚跟,近半年仅售出了 5866 辆,基本退出主流高端纯电轿车竞争序列。反倒是 R7 的表现相对亮眼,以 3 万多辆的成绩排在整个鸿蒙智行体系中的第 4 位。

但只凭借 R7 一款车型,显然难以在激烈的价格战和竞品围剿下突出重围,不仅既无法满足商务接待、多人家庭出行等高价值场景需求,也难以在用户心智中建立高端品牌的完整认知。

正因如此,奇瑞正在以前所未有的力度集中资源来支持智界。

(今年智界)最大的变化是——我听余承东的。

除了暂停原定由星纪元进行的高端 MPV 项目,转而将其交由智界开发外,奇瑞董事长尹同跃还表示奇瑞将在智界品牌上累计投入超 100 亿元资金,并组建一支超过 5000 人的专属研发团队,涵盖智能驾驶、电子电气架构、热管理、座舱生态等核心领域。这一投入强度远超星途或星纪元历史任何项目,甚至接近整个 EXEED 品牌三年的研发总和。

这种史无前例的支持力度背后,是奇瑞和智界对过往双线战略失败的深刻反思。

与智界同期推出的星纪元 ES/ET 系列,与智界 S7/R7 共享平台、动力总成甚至部分供应链,但在营销体系、渠道归属和品牌调性上却各自为战。结果就是导致终端价格混乱、用户认知模糊。2025 年上半年数据显示,星纪元 ES 月销长期徘徊在 1000 辆上下。双线作战并未带来 1+1>2 的效果,反而稀释了本就有限的研发与营销资源。

如今随着资金、团队与独立性的全部到位,奇瑞又迎来了一次机会,而 MPV,正是当前最合适的突破口。

▲ 星途目前的车型序列

MPV 在中国市场是个相对特殊的存在。

在新能源浪潮兴起之前,这一细分市场几乎被丰田、别克等合资品牌垄断,用途也高度集中于公务与商务场景。近年来,随着国产高端 MPV 的崛起,国产品牌开始加速切入中高端市场。然而,与家用 SUV 或轿车领域不同,MPV 市场并未出现国产对合资的「碾压式」替代,而是呈现出多方势力交织、势均力敌的格局。

无论是中国品牌与海外品牌的竞争,家用导向与商用导向的博弈,还是燃油车与新能源车的较量,目前都还处于胶着状态。

数据显示,近半年 MPV 销量冠军仍是丰田赛那,累计售出 46158 辆;别克 GL8 新能源与丰田格瑞维亚销量也均突破 3 万辆,与魏牌高山、腾势 D9 等国产新锐车型差距并不显著。

▲ 近半年的 MPV 销量榜前 6 数据来源:汽车之家

MPV 的核心价值固然在于空间,但要在激烈的竞争中脱颖而出,仅靠「大」远远不够。真正的胜负手,在于能否在空间基础上,叠加更多维度的差异化优势。

传统合资品牌走的是「空间 + 品牌溢价」路线;而国产品牌则尝试了多种组合策略——「空间 + 豪华」、「空间 + 节能」、「空间 + 场景化体验」,以及「空间 + 智能化」。然而至今,尚未有一款车型能在这些维度上做到全面领先、无懈可击。

因此,下一阶段 MPV 市场的破局点,或许就取决于是否会出现一款真正能够打破现有平衡、定义新标准的标杆产品。

那么问题来了:

这台集鸿蒙生态、华为智驾、奇瑞制造与旗舰定位于一身的智界 V9,会成为那个引领变革的「答案」吗?

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

爱范儿 | 原文链接 · 查看评论 · 新浪微博


兴齐眼药:SQ-22031滴眼液干眼Ⅱ期临床试验完成首例受试者入组

2025年12月9日 20:56
36氪获悉,兴齐眼药发布公告,公司研发的SQ-22031滴眼液于今日完成了“一项评估SQ-22031滴眼液治疗中至重度干眼的有效性和安全性的多中心、随机、双盲、安慰剂对照的Ⅱ期临床研究”首例受试者入组,正式进入Ⅱ期临床试验。截至目前,经查询国家药品监督管理局网站,尚无同品种药品批准上市。

思瑞浦:终止筹划购买奥拉股份股权等事项,股票明起复牌

2025年12月9日 20:36
36氪获悉,思瑞浦发布公告,公司因筹划以发行股份及/或支付现金的方式购买奥拉股份股权并募集配套资金事项,公司股票自2025年11月26日开市起开始停牌,公司定向可转换公司债券自2025年11月26日开市起停止转股。预计停牌时间不超过10个交易日。经审慎研究,公司决定终止筹划本次重大资产重组事项,公司股票自2025年12月10日开市起复牌,公司定向可转换公司债券自2025年12月10日开市起恢复转股。
❌
❌