python bytecode解析

前言

我们的电脑是怎么运行的呢?计算机内部的 CPU 处理器是个硅片,上面雕刻着精心布置的电路,输入特定的电流,就能得到另一种模式的电流,而且模式可以预测,给这些模式起上名字并赋予含义,我们就可以说这种电流模式代表加法,电脑的工作原理就是如此,我们起的这些名字叫做 CPU 指令,有时也被成为机器码。[引自:James Bennett] 我们的编程语言是怎么运行的呢?一些语言通过编译器,直接将源代码编译成机器码,这些语言就是编译语言,还有一些语言解除解释器,直接在运行时把源代码解释为机器码,这些就是解释型语言。不过还有第三种语言,介于源代码和机器码之间,一些语言编译得到的指令,但是这种指令不能被现有的CPU直接运行,而需要解释器去理解,并将这些指令翻译为真实的 CPU 接受的二进制码,这种中间指令就是我们今天要说的bytecode(字节码),有很多语言属于此类比如java,C#,还有python。

Java编译的字节码运行在java虚拟机上,C#编译的字节码运行在.Net 虚拟机上,而Python 编译的字节码运行在 Python 虚拟机上。

工作原理

CPython解释器在内部会将Python源代码编译成字节码,并缓存在.pyc​文件中,目的是当再次执行该文件时,直接读取.pyc​文件会更快,这样可以避免从源码重新编译到字节码,当然,Python再找到符合文件后,检查此文件的时间戳,如果发现字节码文件(文件在导入时就被编译完成)比源代码文件时间戳早(比如你修改过原文件),那么就会重新生成字节码,否则就会跳过此步骤。如果,Python在搜索时只找到了字节码而没有找到源代码文件,那么就会直接执行字节码文件(如果没有印象,请回想在模块导入时发生了什么)。然后,Python虚拟机执行字节码编译器发出的字节码。

面向栈

这个是在看码农高天(一个非常厉害的pytohn核心开发者)的视频里学到的概念,CPython使用一个基于栈的虚拟机,也就是说,它完全是面向栈,这种数据结构的。就是不断地push、pop。

CPython使用3种类型的栈:

  • 调用栈(call stack)。这是运行Python程序的主要结构,它为每个当前活动的函数调用,使用了一个东西帧(frame)​,栈底是程序的入口点,每个函数调用推送一个新的帧到调用栈,当函数调用返回后,这个帧被销毁
  • 计算栈(evaluation stack,或称数据栈data stack)。在每个帧中,计算栈就是函数运行的地方,运行的代码大多数是由推入到这个栈中的东西组成的。在栈中操作它们,当函数被返回后,销毁它们。
  • 块栈(block stack)。在每个帧中,块栈被Python用于跟踪某些类型的控制结构,如循环、try/except​块和with ... as ...​块 ,这些控制结构全部被推入到块栈中,当退出这些控制结构式,块栈被销毁,这将帮助Python了解任意给定时刻哪个块是活动的,比如一个continue或者break语句,这些可能影响结果的块。

大多数Python字节码指令操作的是当前调用栈的计算栈,虽然还有些指令可以做其他的事情,比如跳转到指定指令,或者操作块栈。

字节码的阅读

其实还有 代码对象字节码的工作 这两个概念没说,因为本文主要讲怎么阅读字节码,能够通过字节码手搓出py源码(是的,我是个CTFer),想了解更多的可以去本文末的推荐链接里看。

dis模块

因为python代码运行是到字节码再到机器码一气呵成的,我们想要看到中间指令,需要借助python的标准库dis模块,它可以将py代码翻译成字节码。如下:

案例

2024网鼎杯青龙组初赛的MISC02题目(1 解),是一个linux内存镜像取证,前面繁琐的步骤略过,最后一步获得一个 flag.txt 文件,里面是python字节码,明显需要我们手搓还原py代码,如下:

 31         226 PUSH_NULL
228 LOAD_NAME 8 (key_encode)
230 LOAD_NAME 7 (key)
232 PRECALL 1
236 CALL 1
246 STORE_NAME 7 (key) 32 248 PUSH_NULL
250 LOAD_NAME 10 (len)
252 LOAD_NAME 7 (key)
254 PRECALL 1
258 CALL 1
268 LOAD_CONST 7 (16)
270 COMPARE_OP 2 (==)
276 POP_JUMP_FORWARD_IF_FALSE 43 (to 364) 33 278 PUSH_NULL
280 LOAD_NAME 9 (sm4_encode)
282 LOAD_NAME 7 (key)
284 LOAD_NAME 5 (flag)
286 PRECALL 2
290 CALL 2
300 LOAD_METHOD 11 (hex)
322 PRECALL 0
326 CALL 0
336 STORE_NAME 12 (encrypted_data) 34 338 PUSH_NULL
340 LOAD_NAME 6 (print)
342 LOAD_NAME 12 (encrypted_data)
344 PRECALL 1
348 CALL 1
358 POP_TOP
360 LOAD_CONST 2 (None)
362 RETURN_VALUE 32 >> 364 LOAD_CONST 2 (None)
366 RETURN_VALUE Disassembly of <code object key_encode at 0x14e048a00, file "make.py", line 10>:
10 0 RESUME 0 11 2 LOAD_GLOBAL 1 (NULL + list)
14 LOAD_FAST 0 (key)
16 PRECALL 1
20 CALL 1
30 STORE_FAST 1 (magic_key) 12 32 LOAD_GLOBAL 3 (NULL + range)
44 LOAD_CONST 1 (1)
46 LOAD_GLOBAL 5 (NULL + len)
58 LOAD_FAST 1 (magic_key)
60 PRECALL 1
64 CALL 1
74 PRECALL 2
78 CALL 2
88 GET_ITER
>> 90 FOR_ITER 105 (to 302)
92 STORE_FAST 2 (i) 13 94 LOAD_GLOBAL 7 (NULL + str)
106 LOAD_GLOBAL 9 (NULL + hex)
118 LOAD_GLOBAL 11 (NULL + int)
130 LOAD_CONST 2 ('0x')
132 LOAD_FAST 1 (magic_key)
134 LOAD_FAST 2 (i)
136 BINARY_SUBSCR
146 BINARY_OP 0 (+)
150 LOAD_CONST 3 (16)
152 PRECALL 2
156 CALL 2
166 LOAD_GLOBAL 11 (NULL + int)
178 LOAD_CONST 2 ('0x')
180 LOAD_FAST 1 (magic_key)
182 LOAD_FAST 2 (i)
184 LOAD_CONST 1 (1)
186 BINARY_OP 10 (-)
190 BINARY_SUBSCR
200 BINARY_OP 0 (+)
204 LOAD_CONST 3 (16)
206 PRECALL 2
210 CALL 2
220 BINARY_OP 12 (^)
224 PRECALL 1
228 CALL 1
238 PRECALL 1
242 CALL 1
252 LOAD_METHOD 6 (replace)
274 LOAD_CONST 2 ('0x')
276 LOAD_CONST 4 ('')
278 PRECALL 2
282 CALL 2
292 LOAD_FAST 1 (magic_key)
294 LOAD_FAST 2 (i)
296 STORE_SUBSCR
300 JUMP_BACKWARD 106 (to 90) 15 >> 302 LOAD_GLOBAL 3 (NULL + range)
314 LOAD_CONST 5 (0)
316 LOAD_GLOBAL 5 (NULL + len)
328 LOAD_FAST 0 (key)
330 PRECALL 1
334 CALL 1
344 LOAD_CONST 6 (2)
346 PRECALL 3
350 CALL 3
360 GET_ITER
>> 362 FOR_ITER 105 (to 574)
364 STORE_FAST 2 (i) 16 366 LOAD_GLOBAL 7 (NULL + str)
378 LOAD_GLOBAL 9 (NULL + hex)
390 LOAD_GLOBAL 11 (NULL + int)
402 LOAD_CONST 2 ('0x')
404 LOAD_FAST 1 (magic_key)
406 LOAD_FAST 2 (i)
408 BINARY_SUBSCR
418 BINARY_OP 0 (+)
422 LOAD_CONST 3 (16)
424 PRECALL 2
428 CALL 2
438 LOAD_GLOBAL 11 (NULL + int)
450 LOAD_CONST 2 ('0x')
452 LOAD_FAST 1 (magic_key)
454 LOAD_FAST 2 (i)
456 LOAD_CONST 1 (1)
458 BINARY_OP 0 (+)
462 BINARY_SUBSCR
472 BINARY_OP 0 (+)
476 LOAD_CONST 3 (16)
478 PRECALL 2
482 CALL 2
492 BINARY_OP 12 (^)
496 PRECALL 1
500 CALL 1
510 PRECALL 1
514 CALL 1
524 LOAD_METHOD 6 (replace)
546 LOAD_CONST 2 ('0x')
548 LOAD_CONST 4 ('')
550 PRECALL 2
554 CALL 2
564 LOAD_FAST 1 (magic_key)
566 LOAD_FAST 2 (i)
568 STORE_SUBSCR
572 JUMP_BACKWARD 106 (to 362) 18 >> 574 LOAD_CONST 4 ('')
576 LOAD_METHOD 7 (join)
598 LOAD_FAST 1 (magic_key)
600 PRECALL 1
604 CALL 1
614 STORE_FAST 1 (magic_key) 19 616 LOAD_GLOBAL 17 (NULL + print)
628 LOAD_FAST 1 (magic_key)
630 PRECALL 1
634 CALL 1
644 POP_TOP 20 646 LOAD_GLOBAL 7 (NULL + str)
658 LOAD_GLOBAL 9 (NULL + hex)
670 LOAD_GLOBAL 11 (NULL + int)
682 LOAD_CONST 2 ('0x')
684 LOAD_FAST 1 (magic_key)
686 BINARY_OP 0 (+)
690 LOAD_CONST 3 (16)
692 PRECALL 2
696 CALL 2
706 LOAD_GLOBAL 11 (NULL + int)
718 LOAD_CONST 2 ('0x')
720 LOAD_FAST 0 (key)
722 BINARY_OP 0 (+)
726 LOAD_CONST 3 (16)
728 PRECALL 2
732 CALL 2
742 BINARY_OP 12 (^)
746 PRECALL 1
750 CALL 1
760 PRECALL 1
764 CALL 1
774 LOAD_METHOD 6 (replace)
796 LOAD_CONST 2 ('0x')
798 LOAD_CONST 4 ('')
800 PRECALL 2
804 CALL 2
814 STORE_FAST 3 (wdb_key) 21 816 LOAD_GLOBAL 17 (NULL + print)
828 LOAD_FAST 3 (wdb_key)
830 PRECALL 1
834 CALL 1
844 POP_TOP 22 846 LOAD_FAST 3 (wdb_key)
848 RETURN_VALUE magic_key:7a107ecf29325423
encrypted_data:f2c85bd042247896b43345e589e3ad025fba1770e4ac0d274c1f7c2a670830379195aa5547d78bcee7ae649bc3b914da

我们从key_encode​函数源码第11行(字节码第一列是源码行号)开始看:

Disassembly of <code object key_encode at 0x14e048a00, file "make.py", line 10>:
10 0 RESUME 0 11 2 LOAD_GLOBAL 1 (NULL + list)
14 LOAD_FAST 0 (key)
16 PRECALL 1
20 CALL 1
30 STORE_FAST 1 (magic_key)

第十行是定义key_encode​函数,LOAD_GLOBAL​ 加载内置list函数,LOAD_FAST​ 加载key参数,PRECALL​准备调用list函数,参数数量为一,CALL​ 执行函数调用,

STORE_FAST​ 将结果存储在局部变量magic_key中。所以源码就是:

magic_key = list(key)

然后我们看源码第12行:

12          32 LOAD_GLOBAL              3 (NULL + range)
44 LOAD_CONST 1 (1)
46 LOAD_GLOBAL 5 (NULL + len)
58 LOAD_FAST 1 (magic_key)
60 PRECALL 1
64 CALL 1
74 PRECALL 2
78 CALL 2
88 GET_ITER
>> 90 FOR_ITER 105 (to 302)
92 STORE_FAST 2 (i)

前四行LOAD_GLOBAL​、LOAD_CONST​、LOAD_FAST​分别将range、1、len、magic_key压入栈中,即加载,第一组PRECALL​和CALL​是调用len计算magic_key的长度,第二组PRECALL​和CALL​是调用range,GET_ITER、FOR_ITER​开始循环,直到地址302结束,STORE_FAST​将栈顶弹出存入变量 i 中。所以源码为:

magic_key = list(key)
for i in range(1,len(magic_key)):

然后我们看源码第13行:

 13          94 LOAD_GLOBAL              7 (NULL + str)
106 LOAD_GLOBAL 9 (NULL + hex)
118 LOAD_GLOBAL 11 (NULL + int)
130 LOAD_CONST 2 ('0x')
132 LOAD_FAST 1 (magic_key)
134 LOAD_FAST 2 (i)
136 BINARY_SUBSCR
146 BINARY_OP 0 (+)
150 LOAD_CONST 3 (16)
152 PRECALL 2
156 CALL 2
166 LOAD_GLOBAL 11 (NULL + int)
178 LOAD_CONST 2 ('0x')
180 LOAD_FAST 1 (magic_key)
182 LOAD_FAST 2 (i)
184 LOAD_CONST 1 (1)
186 BINARY_OP 10 (-)
190 BINARY_SUBSCR
200 BINARY_OP 0 (+)
204 LOAD_CONST 3 (16)
206 PRECALL 2
210 CALL 2
220 BINARY_OP 12 (^)
224 PRECALL 1
228 CALL 1
238 PRECALL 1
242 CALL 1
252 LOAD_METHOD 6 (replace)
274 LOAD_CONST 2 ('0x')
276 LOAD_CONST 4 ('')
278 PRECALL 2
282 CALL 2
292 LOAD_FAST 1 (magic_key)
294 LOAD_FAST 2 (i)
296 STORE_SUBSCR
300 JUMP_BACKWARD 106 (to 90)

LOAD_GLOBAL​、LOAD_CONST​、LOAD_FAST​ 分别将全局常量str、hex、int压栈,常量'0x'压栈,变量magic_key和 i 压栈,BINARY_SUBSCR​索引动作,即magic_key[i],BINARY_OP​执行+操作,LOAD_CONST​将常量16压栈,PRECALL​和CALL​执行函数调用,在这里我们先暂停一下,强调一下:因为是不断的面向栈操作,我们还原源码时一定要和进栈的顺序对应上,所以我们此时可以还原int('0x'+magic_key[i],16)​,继续往后看,将int、'0x'、magic_key、i、1压栈,BINARY_OP​执行 - 操作,BINARY_SUBSCR​索引,即magic_key[i-1],LOAD_CONST​将16压栈,PRECALL​和CALL​执行函数调用,此时可以还原 int('0x'+magic_key[i-1],16)​,然后BINARY_OP​执行 ^ 操作,两组PRECALL​和CALL​执行函数调用,此时还原到 str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16)))​,LOAD_METHOD​将方法replace压栈,后面将'0x'和''压栈,然后调用函数,即replace('0x',''),然后将magic_key、i压栈,进行索引存储,所以源码为:

magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','')

后面的同理,不再赘叙,第15行、16行 :

magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') for i in range(0,len(key),2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','')

第18行:

magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') for i in range(0,len(key),2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','') magic_key = ''.join(magic_key)

第19行:

magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') for i in range(0,len(key),2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','') magic_key = ''.join(magic_key)
print(magic_key)

第20行:

magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') for i in range(0,len(key),2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','') magic_key = ''.join(magic_key)
print(magic_key)
wdb_key = str(hex(int('0x'+magic_key) ^ int('0x'+key,16))).replace('0x','')

第21行、22行,此时key_encode​函数结束:

def key_encode(key):
magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') for i in range(0,len(key),2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','') magic_key = ''.join(magic_key)
print(magic_key)
wdb_key = str(hex(int('0x'+magic_key) ^ int('0x'+key,16))).replace('0x','')
print(wdb_key)
return wdb_key

然后我们看31行:

key = key_encode(key)

第32行:

key = key_encode(key)
if len(key) == 16:

第33行:

key = key_encode(key)
if len(key) == 16:
encrypted_data = hex(sm4_encode(key,flag))

第34行:

def key_encode(key):
magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') for i in range(0,len(key),2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','') magic_key = ''.join(magic_key)
print(magic_key)
wdb_key = str(hex(int('0x'+magic_key) ^ int('0x'+key,16))).replace('0x','')
print(wdb_key)
return wdb_key key = key_encode(key)
if len(key) == 16:
encrypted_data = hex(sm4_encode(key,flag))
print(encrypted_data)

至此我们的源码就搓出来了,题目还给了如下信息:

magic_key:7a107ecf29325423
encrypted_data:f2c85bd042247896b43345e589e3ad025fba1770e4ac0d274c1f7c2a670830379195aa5547d78bcee7ae649bc3b914da

分析可知,我们已知magic_key​和encrypted_data​,encrypted_data​是由flag​经过sm4加密得到的,密钥为key​,所以我们需要知道key​,就可以sm4解密得到flag​,所以我们需要对 key_encode 函数的逻辑进行逆向得到key​,最后exp如下:

def key_encode(key):
magic_key = list(key)
for i in range(1,len(magic_key)):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') for i in range(0,len(key),2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','') magic_key = ''.join(magic_key)
# print(magic_key)
wdb_key = str(hex(int('0x'+magic_key,16) ^ int('0x'+key,16))).replace('0x','')
# print(wdb_key)
return wdb_key magic_key = list("7a107ecf29325423") for i in range(0,16,2):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i+1],16))).replace('0x','') for i in range(len(magic_key)-1,0,-1):
magic_key[i] = str(hex(int('0x'+magic_key[i],16) ^ int('0x'+magic_key[i-1],16))).replace('0x','') key = "".join(magic_key)
print(key_encode(key)) # 输出:ada1e9136bb16171

然后去赛博厨子解密即可拿到flag:wdflag{815ad4647b0b181b994eb4b731efa8a0}

参考链接:

PyCon 2018:James Bennett--理解 Python 字节码 掘金翻译计划

码农高天:字节码和虚拟机?python代码竟然是这么执行的!

王战山的学习笔记:Python中的字节码

python官方文档:dis — Disassembler for Python bytecode

python bytecode解析的更多相关文章

  1. 用 ElementTree 在 Python 中解析 XML

    用 ElementTree 在 Python 中解析 XML 原文: http://eli.thegreenplace.net/2012/03/15/processing-xml-in-python- ...

  2. Python XML解析(转载)

    Python XML解析 什么是XML? XML 指可扩展标记语言(eXtensible Markup Language). 你可以通过本站学习XML教程 XML 被设计用来传输和存储数据. XML是 ...

  3. python高效解析日志入库

    python脚本解析日志文件入库一般有三个重要的步骤:读文件.解析文件.入库.在这三个方面下功夫,可确保我们获得最优的性能(这里不讨论并发) 1 读文件:一次读一行,磁盘IO太多,效率低下:一次性读如 ...

  4. Python网页解析

    续上篇文章,网页抓取到手之后就是解析网页了. 在Python中解析网页的库不少,我最开始使用的是BeautifulSoup,貌似这个也是Python中最知名的HTML解析库.它主要的特点就是容错性很好 ...

  5. [Python]ConfigParser解析配置文件

    近期发现非常多接口配置都硬编码在souce file中了,于是就看了下python怎么解析配置文件,重构下这一块. 这个应该是早就要作的... 配置文件: [mysqld] user = mysql ...

  6. Python 文本解析器

    Python 文本解析器 一.课程介绍 本课程讲解一个使用 Python 来解析纯文本生成一个 HTML 页面的小程序. 二.相关技术 Python:一种面向对象.解释型计算机程序设计语言,用它可以做 ...

  7. Python XML解析之ElementTree

    参考网址: http://www.runoob.com/python/python-xml.html https://docs.python.org/2/library/xml.etree.eleme ...

  8. python大法好——Python XML解析

    Python XML解析 什么是XML? XML 被设计用来传输和存储数据. XML是一套定义语义标记的规则,这些标记将文档分成许多部件并对这些部件加以标识. 它也是元标记语言,即定义了用于定义其他与 ...

  9. python脚本解析json文件

    python脚本解析json文件 没写完.但是有效果.初次尝试,写的比较不简洁... 比较烦的地方在于: 1,中文编码: pSpecs.decode('raw_unicode_escape') 2,花 ...

  10. python dpkt解析ssl流

    用法:python extract_tls_flow.py -vr  white_pcap/11/2018-01-10_13-05-09_2.pcap  -o pcap_ssl_flow.txt  & ...

随机推荐

  1. 七天.NET 8操作SQLite入门到实战详细教程(选型、开发、发布、部署)

    教程简介 EasySQLite是一个七天.NET 8操作SQLite入门到实战详细教程(包含选型.开发.发布.部署)! 什么是SQLite? SQLite 是一个软件库,实现了自给自足的.无服务器的. ...

  2. 如何用AI抠图助手进行直播--新手指南

    如何用AI抠图助手进行直播 因AI抠图助手目前还没有提供推流直播,所以,可以采用抖音的"手游直播"方式,即抖音开启手游直播后会录制你手机屏幕画面,进行录屏直播:所以我们只要打开AI ...

  3. history的replace("/admin")与("admin")的区别

    假设当前路由为:localhost:3000/index/a 有"/"的情况是直接从根目录替换 改完之后的路由为:localhost:3000/admin 没有"/&qu ...

  4. 编译 Qt 项目

    参考:Qt 编程指南 一个最小化工作示例:qt-minimal | GitHub 源文件 main.cpp #include <QApplication> #include <QLa ...

  5. WSL 使用

    WSL 是一个为在 Windows 10 和 Windows Server 2019 以上能够原生运行 Linux 二进制可执行文件(ELF 格式)的兼容层.可以把它当作一个只能用命令行交互的 Lin ...

  6. Kubernetes-7:Pod健康检查原理-探针(就绪检测、存活检测)

    探针-就绪探测.存活探测 探针是由kubelet对容器执行的定期诊断,要执行诊断,kubelet调用由容器实现的Handler,有三种类型的处理程序: ExecActive:在容器内执行指定命令,若命 ...

  7. parser.add_argument

    parser.add_argument 在解析参数时,有个地方很值得注意. --dict-name,会把dict-name解析为变量dict_name.也就是说会把破折号转成下划线.

  8. RxJS 系列 – 大杂烩

    前言 RxJS 有太多方法了, 想看完整的可以去看 REFERENCE – API List, 这篇介绍一些非 operator 的常用方法. NEVER NEVER.subscribe({ comp ...

  9. 2024.09.18初赛模拟MX-S/P6029记录

    MX-S 太简单了,没啥难度.\yiw $ 1, 3, 5, 7, 9 $ 的二叉搜索树棵数是卡特兰数. P6029 题意 给定一张有 $ n $ 个点,$ m $ 条边的图.可以任意交换途中两条边的 ...

  10. 如何更改Wordpress语言为中文

    在使用WordPress的时候,一般安装默认语言是英文,可以在后台设置里面直接修改站点语言为简体中文,当后台没有语言选项框的这一栏,如下图所示,该怎么办呢? 这个时候我们可以找到文件wp-config ...