news 2026/10/15 1:18:04

MIPS逆向工程实战:从固件分析到ROP利用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MIPS逆向工程实战:从固件分析到ROP利用

1. 为什么MIPS不是“过时的古董”,而是逆向工程师的隐藏训练场

很多人第一次听说MIPS,是在某款老路由器的拆机帖里,或是嵌入式课程PPT角落里一个带箭头的RISC指令图。它不像x86那样充斥着历史包袱,也不像ARM那样被手机芯片厂商轮番刷屏——MIPS安静、克制、逻辑干净,像一本用铅笔写就的工程笔记。正因如此,它成了我带新人做逆向训练时首选的“入门架构”。不是因为它简单,而是因为它诚实:没有微指令翻译层遮掩,没有乱序执行打乱时序,没有SMT线程混淆寄存器状态。你看到的每一条汇编,就是CPU真正执行的那一条。

我带过的某高校嵌入式安全选修课上,A同学第一次反编译一段固件时,面对ARMv7的push {r4-r11, lr}和紧随其后的sub sp, #0x28,花了整整两节课才理清栈帧布局;而同期用MIPS样例练习的B同学,在第三天就能独立识别出sw $ra, 0x20($sp)与lw $ra, 0x20($sp)这对保存/恢复返回地址的对称操作,并准确标出函数入口的栈偏移边界。这不是天赋差异,是架构语义的透明度差异。MIPS的寄存器命名($t0–$t9临时寄存器、$s0–$s7保存寄存器、$a0–$a3参数寄存器)直接映射调用约定,jal跳转必然伴随$ra自动写入,jr $ra就是无条件返回——没有例外,没有隐式行为。

这背后是RISC哲学的彻底贯彻:固定长度指令(32位)、三地址格式、所有运算类指令只操作寄存器、内存访问必须显式通过lw/sw完成。当你在IDA中看到addu $v0, $a0, $a1,你知道它一定不修改标志位,且结果必然存入$v0;看到bne $t0, $zero, loc_400120,你立刻明白这是“若$t0非零则跳转”,连条件码都不用查表。这种确定性,在x86的test eax, eax; jnz或ARM的cmp r0, #0; bne中需要额外心智开销去确认的细节,在MIPS里是刻在指令集DNA里的默认规则。

提示:别被“MIPS已退出主流”误导。它仍在大量网络设备、工业控制器、车载T-Box模块中运行。某次协助某公司分析一款智能电表固件时,我们拿到的.bin文件经file命令识别为ELF 32-bit MSB MIPS, 而其通信协议解析逻辑恰好藏在一段未加壳的mipsel代码段里——因为厂商沿用了十年前的SDK,而那个SDK只支持MIPS小端。真实世界的逆向,永远在和“历史技术债”打交道。

2. 从固件提取到反汇编:MIPS逆向的四步启动链

逆向MIPS固件不是打开IDA点几下就能出伪代码的事。它是一条环环相扣的启动链,任何一环松动,后续分析都会陷入“看似有结果,实则不可信”的陷阱。我把它拆解为四个不可跳过的步骤:固件定位→架构判别→交叉工具链适配→反汇编环境校准。下面以某款家用NAS设备的固件升级包为例,还原真实操作过程。

2.1 固件定位:别急着binwalk,先看文件头签名

拿到一个名为firmware_v2.3.1.bin的文件,第一反应不是binwalk -e,而是用hexdump -C -n 64 firmware_v2.3.1.bin查看前64字节。MIPS固件常见签名有三种:

  • 裸二进制镜像:开头通常是0x00000000(空填充)或0x24080000(addiu $t0,$zero,0,常见于bootloader起始)
  • SREC格式:以S字符开头,如S31500000000...
  • ELF格式:标准ELF魔数7f 45 4c 46(即.ELF)

那次NAS固件的前4字节是4d 49 50 53(ASCIIMIPS),这很可疑——正规ELF不会这样。继续往下看,偏移0x10处出现00 00 00 01,这是MIPS的0x1(即addu $zero,$zero,$zero)的机器码。此时基本可断定是自定义打包格式。我用strings firmware_v2.3.1.bin | grep -i "linux"发现Linux version 3.10.14字样,再结合file命令输出data,确认需手动剥离头部。最终用dd if=firmware_v2.3.1.bin of=kernel.bin bs=1 skip=512跳过512字节头部,得到真正的内核镜像。

2.2 架构判别:readelf比file更可靠

file命令有时会误判。比如某次遇到一个mipsel固件,file显示ELF 32-bit LSB MIPS, 但实际反汇编后函数调用全乱套。原因在于file仅依赖ELF头中的e_machine字段(值为8),而MIPS有大端(mips)和小端(mipsel)两种ABI,e_ident[EI_DATA]字段(第5字节)才是关键:0x01为LSB(小端),0x02为MSB(大端)。正确做法是:

readelf -h kernel.bin | grep -E "(Class|Data|Machine)"

输出中若含Data: 2's complement, little endian且Machine: MIPS R3000,则确认为mipsel。若误用mips工具链反编译mipsel代码,lw $t0,0x10($s0)会被解释为从$s0+0x10读取,但实际内存布局因字节序翻转,真正读取的是$s0+0x01位置——这就是为什么你看到的变量地址总差7位。

2.3 交叉工具链:GNU Binutils的版本陷阱

MIPS逆向离不开objdump、readelf、nm。但不同版本的Binutils对MIPS支持差异极大。例如Binutils 2.25之前,objdump -d对mips32r2指令集的支持不完整,ins(插入位字段)和ext(提取位字段)指令会显示为illegal。我目前稳定使用的组合是:

  • Binutils 2.38:完美支持MIPS32/64 R1-R6全指令集
  • GCC 12.2.0:用于编译测试用例,验证逆向逻辑
  • 配套脚本:mips-linux-gnu-objdump -m mips:isa32r2 -D kernel.bin > disasm.s

关键参数-m mips:isa32r2强制指定指令集版本,避免自动探测失败。曾因漏掉此参数,IDA将rdhwr $v1,$30(读取硬件寄存器)误识别为非法指令,导致整个中断处理函数分析中断。

2.4 反汇编环境校准:IDA Pro的MIPS插件配置要点

IDA默认MIPS加载器常出错。必须手动校准三项:

  1. 处理器类型:Options → Processor type → MIPS R3000 (big-endian)或(little-endian)
  2. 加载地址:MIPS固件通常从0x80000000(KSEG0用户态)或0xbfc00000(BootROM)开始。用readelf -l kernel.bin查看LOAD段的p_vaddr,设为IDA的Base address
  3. 栈指针初始值:在Options → General → Analysis → Stack pointer中填入$sp,否则函数栈帧分析全错

有一次,某固件的p_vaddr是0x80100000,但我误设为0x80000000,导致所有全局变量地址偏移0x100000,调试时printf输出的地址与IDA显示的完全对不上——折腾了六小时才发现是这个基础设置错误。

3. 看懂MIPS汇编:从指令模式到函数调用约定的逐层解剖

MIPS汇编表面简洁,但要真正读懂,必须穿透三层:指令级语义→寄存器级约定→函数级抽象。很多初学者卡在第二层,以为记住lw/sw就足够,却在分析复杂函数时迷失于$fp(帧指针)与$sp(栈指针)的动态关系中。下面用一段真实固件中的密码校验函数(简化版)展开:

.text:00400A80 addiu $sp, $sp, -0x28 .text:00400A84 sw $ra, 0x20($sp) .text:00400A88 sw $s0, 0x1C($sp) .text:00400A8C sw $s1, 0x18($sp) .text:00400A90 move $s0, $a0 # s0 = input_str .text:00400A94 li $s1, 0 # s1 = i = 0 .text:00400A98 .text:00400A98 loc_400A98: # CODE XREF: sub_400A80+2C↓j .text:00400A98 lbu $t0, 0($s0) # t0 = *input_str .text:00400A9C beq $t0, $zero, loc_400AC4 # if *input_str == 0, break .text:00400AA0 addiu $s0, $s0, 1 # input_str++ .text:00400AA4 addiu $s1, $s1, 1 # i++ .text:00400AA8 b loc_400A98 # loop .text:00400AAC nop .text:00400AC4 .text:00400AC4 loc_400AC4: # CODE XREF: sub_400A80+1C↑j .text:00400AC4 move $v0, $s1 # return i (length) .text:00400AC8 lw $ra, 0x20($sp) .text:00400ACC lw $s0, 0x1C($sp) .text:00400AD0 lw $s1, 0x18($sp) .text:00400AD4 addiu $sp, $sp, 0x28 .text:00400AD8 jr $ra .text:00400ADA nop

3.1 指令级:理解lbu与lb的本质区别

第0x400A98行的lbu $t0, 0($s0)是关键。lbu(Load Byte Unsigned)将内存中1字节数据零扩展至32位存入寄存器;而lb(Load Byte)是符号扩展。在字符串处理中,若用lb读取0xFF(ASCII DEL),结果是0xFFFFFFFF(-1),导致后续比较出错。MIPS设计者刻意区分二者,正是为避免C语言中char有符号/无符号的歧义。实操中,只要涉及字符串、二进制数据,一律优先用lbu/sb(Store Byte)。

3.2 寄存器级:$s0/$s1为何能跨函数调用?

MIPS ABI(Application Binary Interface)规定:$s0–$s7为调用者保存寄存器(callee-saved)。这意味着函数内部可以随意使用它们,但返回前必须恢复原始值。看0x400A84–0x400A8C三行sw指令,正是将$ra、$s0、$s1压栈保存;而末尾0x400AC8–0x400AD0的lw则是恢复。若忽略此约定,分析递归函数时,$s0的值在深层调用中被覆盖,回溯时就会得到错误的字符串地址。

3.3 函数级:栈帧结构的可视化还原

这段代码构建了一个标准MIPS栈帧:

高地址 +------------------+ | $ra (saved) | ← $sp + 0x20 +------------------+ | $s0 (saved) | ← $sp + 0x1C +------------------+ | $s1 (saved) | ← $sp + 0x18 +------------------+ | ... | +------------------+ | 局部变量空间 | ← $sp + 0x00 (当前栈顶) 低地址

addiu $sp, $sp, -0x28分配32字节栈空间,其中0x20字节用于保存寄存器,剩余0x8字节可作局部变量。$fp(帧指针)在此例中未使用,因函数无复杂局部变量;但若出现int buf[10],编译器会生成move $fp, $sp并用$fp作为基址访问数组。

注意:MIPS没有push/pop指令,所有栈操作必须显式计算偏移。初学者常犯错误是把sw $ra, 0x20($sp)写成sw $ra, 0($sp),导致覆盖其他保存寄存器。记住口诀:“先减sp再存,先取再加sp”。

4. 动态调试实战:QEMU+GDB搭建MIPS全链路调试环境

静态反汇编只能看到代码逻辑,而真实漏洞往往藏在运行时状态中。比如某次分析一个UPnP服务崩溃问题,静态看strcpy调用完全合规,但GDB调试时发现$a0(目标缓冲区地址)在特定请求下竟指向只读内存段——这只能在运行时捕获。为此,我构建了一套基于QEMU的MIPS动态调试环境,核心是解决三个痛点:固件加载地址对齐、系统调用拦截、GDB断点稳定性。

4.1 QEMU系统模式:选择malta还是virt?

MIPS虚拟化有两个主流平台:

  • malta:模拟真实的MIPS Malta开发板,支持PCI、IDE等复杂外设,但启动慢(需加载完整内核+initramfs),适合分析驱动级漏洞
  • virt:轻量级虚拟平台,类似ARM的virtio,启动快(<2秒),适合应用层调试

我的选择原则:若固件是完整Linux系统(含/sbin/init),用malta;若只是单个用户态程序(如/bin/httpd),用virt。后者配置更简单:

qemu-system-mipsel -M virt -m 512M \ -kernel vmlinux-3.10-mipsel \ -initrd initramfs.cgz \ -append "console=ttyS0" \ -nographic \ -S -s # -S暂停启动,-s开启GDB server(端口1234)

4.2 GDB远程调试:绕过QEMU的符号表缺失陷阱

QEMU启动后,GDB连接target remote :1234,但info functions为空——因为内核未加载符号表。解决方案分三步:

  1. 获取内核符号:cat /proc/kallsyms > kallsyms.txt(需固件支持/proc)
  2. 加载符号到GDB:(gdb) add-symbol-file vmlinux-3.10-mipsel 0x80100000 -s .text 0x80100000 -s .data 0x80f00000
  3. 设置物理地址断点:MIPS内核使用虚拟地址,但QEMU GDB server调试的是物理内存。用monitor info mem查看内存映射,将虚拟地址0x80400A80转换为物理地址(通常减0x80000000)再下断点:(gdb) hb *0x400A80

4.3 关键调试技巧:监控内存越界与寄存器污染

MIPS逆向中最难复现的bug是寄存器污染。例如某次调试发现$t0在函数返回后值异常,追踪发现是调用printf时,其内部汇编未遵守ABI规范,未保存$t0。此时需启用QEMU的寄存器监控:

qemu-system-mipsel -d in_asm,cpu -D qemu.log ...

日志中搜索$t0的赋值行,定位到printf内部的move $t0, $a0后未恢复,证实了ABI违规。另一个技巧是用GDB的watch命令监控栈内存:

(gdb) watch *(int*)($sp+0x20) # 监控$ra保存位置 (gdb) commands Type commands for breakpoint(s) 1, one per line. End with a line saying just "end". >printf "RA overwritten at %x\n", $pc >continue >end

当$ra被意外改写时,立即打印当前指令地址,精准定位溢出点。

4.4 实战案例:定位一个堆溢出漏洞

某固件的Web管理界面存在POST /cgi-bin/admin接口,发送超长password参数会导致设备重启。静态分析发现cgi-bin/admin调用parse_form_data,该函数中有一段:

li $t0, 0x200 # alloc 512 bytes jal malloc move $s0, $v0 # s0 = buffer ... lbu $t1, 0($a1) # a1 = user_input sb $t1, 0($s0) # store to buffer addiu $a1, $a1, 1 addiu $s0, $s0, 1 bne $t1, $zero, loop

表面看是安全的,但malloc(0x200)返回的缓冲区在堆上,而sb指令无边界检查。GDB中设置watch *(char*)$s0,发送password=A*1000,触发断点时$s0已超出分配区域。用info proc mappings确认堆地址范围,计算出溢出偏移为0x200,最终构造shellcode覆盖malloc的free_list指针,实现任意地址写入。

5. 从逆向到利用:ROP链构造与MIPS特殊约束

MIPS的ROP(Return-Oriented Programming)与x86/x64有本质不同:它没有pop指令,所有寄存器加载必须通过lw从栈中读取,且jr $ra是唯一的返回指令。这意味着MIPS ROP链必须严格满足两个约束:栈平衡与寄存器可控性。我曾在一个路由器提权漏洞中,用17个gadget拼出完整的mprotect调用链,下面拆解其设计逻辑。

5.1 MIPS ROP的核心gadget类型

MIPS中有效的ROP gadget必须以jr $ra结尾,且中间不能破坏关键寄存器。常见有效gadget:

  • Load-into-register:lw $t0, offset($sp); jr $ra—— 将栈上offset处的值载入$t0
  • Add-to-register:addu $t0, $t0, $t1; jr $ra—— $t0 += $t1
  • System-call setup:move $a0, $t0; move $a1, $t1; move $a2, $t2; jr $ra—— 设置系统调用参数

注意:li $t0, value(立即数加载)在MIPS中是伪指令,实际由lui+ori两条指令组成,中间可能破坏其他寄存器,故不可靠。必须用lw从栈加载常量。

5.2 栈平衡:为什么MIPS ROP链长度必为奇数?

MIPS调用约定中,jal指令将返回地址存入$ra,而jr $ra返回。但每个gadget执行jr $ra后,$ra仍指向下一个gadget地址。因此,ROP链必须保证每次jr $ra后,$ra能指向正确的下一个gadget。由于jr $ra不改变栈指针,而每个gadget需从栈读取参数,必须用addiu $sp, $sp, N调整栈顶。典型gadget结构:

lw $t0, 0x0($sp) # 加载参数1 lw $t1, 0x4($sp) # 加载参数2 addiu $sp, $sp, 0x8 # 平衡栈,跳过已用参数 jr $ra

addiu $sp, $sp, 0x8使栈指针前进8字节,正好跳过两个32位参数。若链中某个gadget遗漏此指令,后续所有gadget的lw都会读取错误数据。这就是为什么MIPS ROP链长度常为奇数——需用addiu $sp精确控制栈偏移。

5.3 实战:构造mprotect(0x400000, 0x1000, 7)调用链

目标是将地址0x400000开始的0x1000字节内存设为可读可写可执行(prot=7)。需设置$a0=0x400000,$a1=0x1000,$a2=7,然后触发syscall 4000(mprotect)。gadget选取:

  • gadget1:lw $a0, 0x0($sp); addiu $sp, $sp, 4; jr $ra(加载$a0)
  • gadget2:lw $a1, 0x0($sp); addiu $sp, $sp, 4; jr $ra(加载$a1)
  • gadget3:lw $a2, 0x0($sp); addiu $sp, $sp, 4; jr $ra(加载$a2)
  • gadget4:li $v0, 4000; syscall; jr $ra(系统调用)

ROP链布局(栈内容,从低地址到高地址):

低地址 +------------------+ | gadget1 addr | ← $sp+0x00 (第一个jr $ra跳到这里) +------------------+ | 0x400000 | ← $sp+0x04 (gadget1的lw $a0,0x0($sp)) +------------------+ | gadget2 addr | ← $sp+0x08 (gadget1的jr $ra跳到这里) +------------------+ | 0x1000 | ← $sp+0x0C (gadget2的lw $a1,0x0($sp)) +------------------+ | gadget3 addr | ← $sp+0x10 (gadget2的jr $ra跳到这里) +------------------+ | 0x00000007 | ← $sp+0x14 (gadget3的lw $a2,0x0($sp)) +------------------+ | gadget4 addr | ← $sp+0x18 (gadget3的jr $ra跳到这里) +------------------+ | ... | 高地址

执行流程:gadget1加载$a0=0x400000,$sp增至$sp+4;jr $ra跳至gadget2地址,此时$sp指向0x1000,gadget2加载$a1后$sp再+4;以此类推,最终gadget4执行syscall。整个链共4个gadget,需在栈上布置7个数据项(4个gadget地址+3个参数),符合“奇数长度”规律。

经验:MIPS ROP调试时,用x/20xw $sp持续观察栈内容变化,比单步执行更高效。一旦发现$sp未按预期增长,立即检查gadget中是否遗漏addiu $sp指令。

6. 避坑指南:MIPS逆向中五个血泪教训与对应解法

在数十个MIPS固件分析项目中,我总结出五个高频致命坑。它们不常出现在教材里,却是真实逆向中消耗最多时间的环节。每个坑都附带可立即复用的检测脚本或命令。

6.1 坑:.rodata段被误认为代码段,导致IDA反汇编失败

现象:IDA将字符串常量区(如"admin"、"/tmp/")识别为代码,生成大量invalid instruction。这是因为MIPS固件常将.rodata与.text段连续存放,且无明确段边界标记。readelf -S显示.rodata的sh_flags为A(allocatable),但IDA默认将其当作可执行段。

解法:在IDA中Edit → Segments → Create segment,手动创建.rodata段,起始地址填readelf -S kernel.bin | grep rodata | awk '{print $4}',权限设为Read,取消勾选Execute。之后Right-click → Convert to string批量转为字符串。

6.2 坑:jal指令的目标地址计算错误,函数调用跳转到错误位置

MIPS的jal是26位立即数跳转,实际跳转地址为((imm << 2) | (pc & 0xf0000000))。初学者常忽略PC的高位继承,将jal 0x123456直接算成0x123456,而实际是0x80123456(假设PC在0x80000000段)。这导致IDA无法识别函数边界。

解法:用Python脚本自动修正:

def jal_target(pc, imm): return ((imm << 2) | (pc & 0xf0000000)) # 示例:pc=0x80400000, imm=0x10000 → target=0x80440000

在IDA中,对疑似jal指令按Y键,手动输入正确的目标地址。

6.3 坑:memcpy等libc函数被编译器内联,静态分析找不到调用点

现象:代码中有明显内存拷贝逻辑,但nm kernel.bin | grep memcpy无输出。这是因为GCC对短拷贝(<64字节)默认内联为lw/sw循环,而非调用外部函数。

解法:搜索lw/sw密集出现的循环模式。用grep -A5 -B5 "lw.*\$t" disasm.s | grep -A10 "sw.*\$t"定位。内联memcpy通常有固定特征:$t0/$t1作源/目标地址,$t2作计数器,循环体含lw $t3,0($t0); sw $t3,0($t1); addiu $t0,1; addiu $t1,1; addiu $t2,-1。

6.4 坑:delay slot指令被忽略,导致条件跳转逻辑误判

MIPS所有分支指令(beq,bne,bgtz等)后跟一个延迟槽(delay slot),该指令无论分支是否发生都会执行。例如:

beq $t0, $t1, label addiu $t2, $t2, 1 # 这条总会执行! label:

初学者常以为addiu只在跳转时执行,导致逻辑分析错误。

解法:在IDA中Options → General → Analysis → Enable delay slot analysis必须勾选。若关闭,IDA会将addiu显示为label的首条指令,造成严重误导。

6.5 坑:mipsel固件在mipsIDA中反汇编,sw $t0,0x10($s0)被解释为sw $t0,0x01($s0)

字节序错误是最隐蔽的坑。mipsel的sw指令机器码ad080010,在mips(大端)解释下,0010被读作0x0010,而在mipsel(小端)中,10 00应读作0x0010——等等,这似乎一样?不,关键在多字节立即数。sw $t0,0x1234($s0)在mipsel中机器码为ad083412,大端解释为0x3412,小端解释为0x1234。差0x21e2!

解法:用xxd -e(小端)与xxd -E(大端)对比同一段机器码。若xxd -e输出00000010而xxd -E输出10000000,则确认为小端。此时必须用mipsel工具链及IDA的little-endian模式。

7. 工具链与资源:一份可直接部署的MIPS逆向工作箱

最后,分享我日常使用的MIPS逆向工具箱。所有工具均经生产环境验证,附带一键部署脚本。拒绝“理论推荐”,只列实测有效的方案。

7.1 基础工具链(Ubuntu 22.04 LTS)

# 安装交叉编译工具链(Debian/Ubuntu) sudo apt install gcc-mips-linux-gnu binutils-mips-linux-gnu # 验证安装 mips-linux-gnu-gcc --version # 应输出gcc 12.2.0 mips-linux-gnu-objdump --help | head -5 # 推荐替换为更稳定的Buildroot工具链(下载buildroot-2023.02.tar.bz2) make menuconfig # 在Toolchain中选择MIPS 32r2, little-endian make -j$(nproc) # 生成路径:output/host/bin/mips-linux-gcc

7.2 静态分析工具

  • Ghidra 10.3+:原生支持MIPS,Decompiler对switch语句还原极佳。配置:File → Import File → Language: MIPS:LE:32:Default:default
  • Radare2:命令行利器,r2 -A -a mips -b 32 firmware.bin自动分析。aaa(分析所有)后,afl(函数列表)准确率超IDA
  • Custom Python Script:检测硬编码密钥
    import re with open("firmware.bin", "rb") as f: data = f.read() # 匹配16字节以上连续非零字节(常见密钥) keys = re.findall(b'[\x01-\xff]{16,}', data) for k in keys[:5]: print("Possible key:", k.hex())

7.3 动态调试工具

  • QEMU 7.2.0:编译时启用--enable-debug,--target-list=mips-softmmu,mipsel-softmmu
  • GDB 12.1:编译时加--enable-targets=all,支持mips:isa32r2
  • QEMU Monitor Commands(运行时输入Ctrl+A, C进入monitor):
    • info registers:查看所有寄存器
    • xp/10wx 0x80000000:以16进制打印内存
    • memsave 0x80000000 0x1000 dump.bin:保存内存到文件

7.4 学习资源与测试固件

  • 官方文档:MIPS Architecture Volume III: “M
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 1:17:53

OCR与STR本质区别:文档识别vs场景文本理解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:16:28

Java实现BD-09、GCJ-02与WGS84坐标系精准互转

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:15:55

如何构建第一时间发现线上故障的监控告警体系

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:15:46

第一次投期刊要查AI率吗?期刊AIGC检测与降AI全流程

期刊AI率超标被退稿&#xff0c;往往不是因为作者用了AI&#xff0c;而是投稿前根本没查过。 自己写的论文同样可能有AI率&#xff0c;20%-30%的情况并不少&#xff0c;尤其是综述和总结部分&#xff0c;表达越规整越容易被误判为AIGC生成。 每个期刊的AIGC率要求是多少&…

作者头像 李华
网站建设 2026/10/15 1:15:02

数据仓库命名规范实战:分层、业务域与字段命名避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:14:46

两串锂电池主动均衡方案:MP2642GR-Z与PIC18LF46K80的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华