动态链接

计算机程序链接时分两种形式:静态链接和动态链接。

静态链接在链接时将所有目标文件中的代码、数据等Section都组装到可执行文件当中,并将代码中使用到的外部符号(函数、变量)都进行了重定位。因此在执行时不需要依赖其他外部模块即可执行,并且可以获得更快的启动时间和执行速度。然而静态链接的方式缺点也很明显:

  • 模块更新困难。如果依赖的外部函数有着严重的bug,那么不得不与修复的外部模块重新链接生成新的可执行文件。
  • 对磁盘和内存的浪费非常严重。每一个可执行文件如果都包含C语言静态库,那么对于 /usr/bin 目录下上千个可执行文件,最后造成的浪费是不可想象的。

    动态链接将程序模块相互分割开来,而不再将它们静态的链接在一起,等到程序要运行时才链接。尽管相比于静态链接由于需要在运行时进行链接带来了一定的性能损耗,但其能够有效的节省内存以及动态更新,因此有着广大的应用。

延迟绑定(PLT/GOT 表)

在动态链接下,程序模块之间包含大量的符号引用,所以在程序开始执行前,动态链接会耗费不少时间用于解决模块间的符号引用的查找和重定位。在一个程序中,并非所有的逻辑都会走到,可能直到程序执行完成,很多符号(全局变量或函数)都并未被执行(比如一些错误分支)。因此如果在程序执行前将所有的外部符号都进行链接,无疑是一种性能浪费,同时也极大地拖累了启动速度。所以 ELF 采用了一种延迟绑定(Lazy Binding)的做法,思想也比较简单,当外部符号第一次使用时进行绑定(符号查找、重定位等),第二次使用时直接使用第一次符号绑定的结果。

站在编译器的角度来看,由于编译器在链接阶段无法得知外部符号的地址,因此其在编译时发现有对外部符号的引用,将生成一小段代码,用于外部符号的重定位。

ELF 使用 PLT(Procedure Linkage Table 过程链接表) 和 GOT(Global Offset Table 全局偏移表) 来实现延迟绑定:

  • PLT表:编译器生成的用于获取数据段中外部符号地址的一小段代码组成的表格。它使得代码可以方便地访问共享的符号。对于每一个引用的共享符号,PLT 表都会有一个对应的条目。这些条目用于管理和重定位动态链接的符号。
  • GOT表:存放外部符号地址的数据段。

为什么需要 PLT/GOT 表

在不熟悉现代操作系统对于内存的访问控制权限的情况下,我们可能会有疑惑:

  • 只通过 PLT 表无法实现延迟绑定吗?
  • PLT 表重定位拿到外部符号的地址后,再次访问时跳转到对应的地址不行吗?

    这里主要有两个原因。

代码段访问权限的限制

一般来说,代码段:可读、可执行;数据段:可读、可写。PLT 表项进行重定位后,要使得下次访问外部符号时直接跳转到重定位后的地址,需要对代码段进行修改。然而代码段是没有写权限的。既然代码段没有写权限而数据段是可写的,那么在代码段中引用的外部符号,可以在数据段中增加一个跳板:让代码段先引用数据段中的内容,然后在重定位时,把外部符号重定位的地址填写到数据段中对应的位置。这个过程正好对应 PLT/GOT 表的用途。

以下为一个基本示意图,实际的 PLT/GOT 流程更为复杂。

+------------------+     +-------------------+     +-----------------+     +---------------------+
| | | | | | | |
| printf_func | +--+-> printf@plt | | printf@got | +---+--> f73835f0<printf> |
| | | | | | | | | |
| call printf@plt +--+ | jmp *printf@got-+-----+-> 0xf7e835f0----+-+ | |
| | | | | | | |
+------------------+ +-------------------+ +-----------------+ +---------------------+
可执行文件 PLT 表 GOT 表 glibc中的printf

共享内存的考虑

即使可以对代码段进行修改,由于 PLT 代码片段是在一个共享对象内,因为代码段被修改了,就无法实现所有进程共享同一个共享对象。而动态库的主要优点之一是多个进程共享同一个共享对象的代码段,拥有数据段的独立副本,从而节省内存空间。为了解决这个问题,PLT/GOT 表的使用变得必要。通过在数据段中增加一个全局偏移表(GOT),在程序运行时进行动态重定位,从而实现多个进程共享同一个共享对象的代码段,而数据段仍然保持独立副本。

PLT/GOT 表工作原理

概述

当程序要调用一个外部函数时,它会首先跳转到 PLT 表中的对应条目。当 PLT 表中的条目被调用时,它会首先检查 GOT 表中是否已经存在该函数的地址。如果存在,PLT 将直接跳转到该地址;否则 PLT 将调用动态链接器 (dynamic linker)寻找该函数的地址,并将该地址填充到 GOT 表中。

当函数的地址被填充到 GOT 表中后,下一次调用该函数时,PLT 将直接跳转到该地址,而不需要再次调用动态链接器。这个过程中,GOT 表充当了一个缓存,可以避免重复调用动态链接器,从而提高程序的执行效率。

为了更好地理解 PLT/GOT 的工作原理,下面是一个示意图:

  第一次对外部符号进行调用            第二次对同一外部符号进行调用
┌──────────────────────┐ ┌───────────────────┐
│ External Func │ │ External Func Addr│
└──────────────────────┘ └───────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────┐
│ PLT Stub │ │ PLT Stub │
└──────────────────────┘ └───────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────┐
│ GOT Entry Addr │ │ GOT Entry Addr │
└──────────────────────┘ └───────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────┐
│ Dynamic Linker Call │ │ External Func Addr│
└──────────────────────┘ └───────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌───────────────────┐
│ Update GOT Entry │ │ Call Func │
└──────────────────────┘ └───────────────────┘


┌──────────────────────┐
│ External Func Addr │
└──────────────────────┘


┌──────────────────────┐
│ Call Func │
└──────────────────────┘

实际上 ELF 将 GOT 拆分成了两个表: .got 和 .got.plt 。其中:

  • .got 用来保存全局外部变量的引用地址
  • .got.plt 用来保存外部函数引用的地址。

    我们这里阐述的默认都是指的外部函数调用的流程。

工作流程

由延迟绑定的基本思想可以知道,第二次使用共享符号时不会再次进行重定位。那么 GOT 表是如何判断是否是第一次访问的共享符号的呢?

一种常规的思想就是共享符号对应的 GOT 表项设置一个特殊的初始值,由于重定位后会更新共享符号的地址,因此判断 GOT 表项中是否是这个初始值,是的话即为第一次访问。那 ELF 文件中实际是如何处理的呢?

对于一个 PLT 表项,其包含三条指令,格式如下:

addr xxx@plt
jmp *(xxx@got)
push offset
jmp *(_dl_runtime_resolve)

指令一

指令一跳转到一个地址,这个地址的值从对应的 GOT 表项中读取。这条 GOT 表项初始存储的是 PLT 表项第二条指令的地址。因此实际相当于直接顺序执行第二条指令。当重定位后 GOT 表项中存储的地址会被更新为外部符号的实际地址。因此后续访问这个外部符号时,指令一将直接跳转到对应的外部符号地址。通过这种巧妙的方式在延迟初始化的时候避免了每次都进行重定位。

+-------------------------------+    +-----------+       +--------------+
| 1 | | | | |
| addr puts@plt +-------------+----> puts@got | | <printf> |
| | | | | | |
| jpm *(puts@got) | | | | |
| 2 | | | 5 | |
| push offset<------------------+----+ +------>| |
| |3 | | | | |
| v | +--+--------+ +--------------+
| jmp *(__dl_runtime_resolve)| |
| | | 4 |
| +--+-------+
| | update addr
| |
+-------------------------------+

指令二

指令二会压入一个操作数,这个操作数实际是外部符号的标识符id,动态链接器通过它来区分要解析哪个外部符号以及解析完后需要更新哪个 GOT 表项的数据。这个操作数通常是这个函数在 .rel.plt 的下标或地址,通过 readelf -r elf_file 可以查看 .rel.plt 信息。

指令三

指令三会跳转到一个地址,这个地址是动态链接做符号解析和重定位的公共入口。因为所有的外部函数都需要经历这一步骤,因此被提炼为公共函数,而非每个 PLT 表项都有一份重复指令。实际上这个公共入口指向 _dl_runtime_resolve,其完成符号解析和重定向工作后,将外部函数的真实地址填到对应的 GOT 表项中。

案例分析

这里以 32 位 ELF 可执行文件进行分析。

#include <stdio.h>

int main(){
printf("Hello World\n");
printf("Hello World Again\n"); return 0;
}
# 编译
gcc -Wall -g -o test.o -c test.c -m32 # 链接,添加 -z lazy,这样在 gdb 调试时才可以看到延迟绑定的过程
gcc -o test test.o -m32 -z lazy

查看 test 可执行文件的汇编代码 objdump -d test,其输出的部分结果如下

PLT 表的汇编如下:

Disassembly of section .plt:

00001030 <__libc_start_main@plt-0x10>:
1030: ff b3 04 00 00 00 push 0x4(%ebx)
1036: ff a3 08 00 00 00 jmp *0x8(%ebx)
103c: 00 00 add %al,(%eax)
... 00001040 <__libc_start_main@plt>:
1040: ff a3 0c 00 00 00 jmp *0xc(%ebx)
1046: 68 00 00 00 00 push $0x0
104b: e9 e0 ff ff ff jmp 1030 <_init+0x30> 00001050 <puts@plt>:
1050: ff a3 10 00 00 00 jmp *0x10(%ebx)
1056: 68 08 00 00 00 push $0x8
105b: e9 d0 ff ff ff jmp 1030 <_init+0x30> Disassembly of section .plt.got: 00001060 <__cxa_finalize@plt>:
1060: ff a3 18 00 00 00 jmp *0x18(%ebx)
1066: 66 90 xchg %ax,%ax

代码段 main 部分的汇编如下:

0000119d <main>:
119d: 8d 4c 24 04 lea 0x4(%esp),%ecx
11a1: 83 e4 f0 and $0xfffffff0,%esp
11a4: ff 71 fc push -0x4(%ecx)
11a7: 55 push %ebp
11a8: 89 e5 mov %esp,%ebp
11aa: 53 push %ebx
11ab: 51 push %ecx
11ac: e8 ef fe ff ff call 10a0 <__x86.get_pc_thunk.bx>
11b1: 81 c3 4f 2e 00 00 add $0x2e4f,%ebx
11b7: 83 ec 0c sub $0xc,%esp
11ba: 8d 83 08 e0 ff ff lea -0x1ff8(%ebx),%eax
11c0: 50 push %eax
11c1: e8 8a fe ff ff call 1050 <puts@plt>
11c6: 83 c4 10 add $0x10,%esp
11c9: 83 ec 0c sub $0xc,%esp
11cc: 8d 83 14 e0 ff ff lea -0x1fec(%ebx),%eax
11d2: 50 push %eax
11d3: e8 78 fe ff ff call 1050 <puts@plt>
11d8: 83 c4 10 add $0x10,%esp
11db: b8 00 00 00 00 mov $0x0,%eax
11e0: 8d 65 f8 lea -0x8(%ebp),%esp
11e3: 59 pop %ecx
11e4: 5b pop %ebx
11e5: 5d pop %ebp
11e6: 8d 61 fc lea -0x4(%ecx),%esp
11e9: c3 ret

查看 test 可执行文件的重定位信息 readelf -r test,其输入部分如下:

Relocation section '.rel.dyn' at offset 0x384 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
00003ef4 00000008 R_386_RELATIVE
00003ef8 00000008 R_386_RELATIVE
00003ff8 00000008 R_386_RELATIVE
00004018 00000008 R_386_RELATIVE
00003fec 00000206 R_386_GLOB_DAT 00000000 _ITM_deregisterTM[...]
00003ff0 00000306 R_386_GLOB_DAT 00000000 __cxa_finalize@GLIBC_2.1.3
00003ff4 00000506 R_386_GLOB_DAT 00000000 __gmon_start__
00003ffc 00000606 R_386_GLOB_DAT 00000000 _ITM_registerTMCl[...] Relocation section '.rel.plt' at offset 0x3c4 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000400c 00000107 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.34
00004010 00000407 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0

通过 gdb 调试,在执行 printf("Hello World\n") 时可以看到其调用了 call 0x56556050 <puts@plt>

─── Output/messages ─────────────────────────────────────────────────────────────────────────────────────────────
0x565561c1 4 printf("Hello World\n");
─── Assembly ────────────────────────────────────────────────────────────────────────────────────────────────────
0x565561ac main+15 call 0x565560a0 <__x86.get_pc_thunk.bx>
0x565561b1 main+20 add $0x2e4f,%ebx
!0x565561b7 main+26 sub $0xc,%esp
0x565561ba main+29 lea -0x1ff8(%ebx),%eax
0x565561c0 main+35 push %eax
0x565561c1 main+36 call 0x56556050 <puts@plt>
0x565561c6 main+41 add $0x10,%esp
0x565561c9 main+44 sub $0xc,%esp
0x565561cc main+47 lea -0x1fec(%ebx),%eax
0x565561d2 main+53 push %eax
─── Breakpoints ─────────────────────────────────────────────────────────────────────────────────────────────────
[1] break at 0x565561b7 in test.c:4 for main hit 1 time
─── Expressions ──────────────────────────────────────────────────────────────────────────────────────────────────
─── History ──────────────────────────────────────────────────────────────────────────────────────────────────────
─── Memory ───────────────────────────────────────────────────────────────────────────────────────────────────────
─── Registers ────────────────────────────────────────────────────────────────────────────────────────────────────
eax 0x56557008 ecx 0xffffcf00 edx 0xffffcf20 ebx 0x56559000
esp 0xffffced0 ebp 0xffffcee8 esi 0xffffcfb4
edi 0xf7ffcb80 eip 0x565561c1 eflags [ PF AF SF IF ] cs 0x00000023
ss 0x0000002b ds 0x0000002b es 0x0000002b
fs 0x00000000 gs 0x00000063
─── Source ───────────────────────────────────────────────────────────────────────────────────────────────────────
~
~
1 #include <stdio.h>
2
3 int main(){
!4 printf("Hello World\n");
5 printf("Hello World Again\n");
6
7 return 0;
8 }
─── Stack ──────────────────────────────────────────────────────────────────────────────────────────────────────────
[0] from 0x565561c1 in main+36 at test.c:4
─── Threads ────────────────────────────────────────────────────────────────────────────────────────────────────────
[1] id 5467 name test from 0x565561c1 in main+36 at test.c:4

disassemble 0x56556050 查看一下 puts@plt 中的内容,其包含三条指令。

>>> disassemble 0x56556050
Dump of assembler code for function puts@plt:
0x56556050 <+0>: jmp *0x10(%ebx)
0x56556056 <+6>: push $0x8
0x5655605b <+11>:jmp 0x56556030
End of assembler dump.

指令一执行了 jmp *0x10(%ebx),其表示跳转到一个地址,地址值为存储在 ebx 寄存器中的值加上 0x10。

通过 info registers 查看寄存器的值为 0x56559000 加上 0x10 后最终地址为 0x56559010。通过 x 0x56559010 查看这个地址的内容为 0x56556056,这个地址也即 puts@plt 中第二条指令的位置。

>>> info registers
eax 0x56557008 1448439816
ecx 0xffffcf00 -12544
edx 0xffffcf20 -12512
ebx 0x56559000 1448448000
esp 0xffffced0 0xffffced0
ebp 0xffffcee8 0xffffcee8
esi 0xffffcfb4 -12364
edi 0xf7ffcb80 -134231168
eip 0x565561c1 0x565561c1 <main+36>
eflags 0x296 [ PF AF SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
>>> x 0x56559010
0x56559010 <puts@got.plt>: 0x56556056

指令二压入 printf 的标识符。

指令三跳转到一个地址为 0x56556030,这个地址为动态链接器做符号解析和重定位的入口。其与 puts@plt 地址 0x56556050 相差 0x20 ,而这个数值正好等于汇编代码中 00001030 <__libc_start_main@plt-0x10>:00001050 <puts@plt>: 的差值。

对于 _dl_runtime_resolve 的执行过程我们不去探究,其在符号解析和重定位结束后会根据指令二压入的操作数标识符更新 GOT 表项的地址。

>>> x /5i 0x56556030
=> 0x56556030: push 0x4(%ebx)
0x56556036: jmp *0x8(%ebx)
0x5655603c: add %al,(%eax)
0x5655603e: add %al,(%eax)
0x56556040 <__libc_start_main@plt>: jmp *0xc(%ebx)

基于 PLT/GOT 机制进行 hook

测试程序

创建一个共享库 libtest.so,由 test.htest.c 组成。

# 编译生成 libtest.so
gcc test.h test.c -fPIC -shared -o libtest.so
// test.h
#ifndef TEST_H
#define TEST_H 1 #ifdef __cplusplus
extern "C" {
#endif void say_hello(); #ifdef __cplusplus
}
#endif #endif
// test.c
#include <stdlib.h>
#include <stdio.h> void say_hello()
{
char *buf = malloc(1024);
if(NULL != buf)
{
snprintf(buf, 1024, "%s", "hello\n");
printf("%s", buf);
}
}

创建一个测试程序 main,其调用了 libtest.so 中的函数。

# 编译生成执行文件
gcc main.c -L. -ltest -o main # 添加 libtest.so 路径,使so可被动态链接
export LD_LIBRARY_PATH=/path/to/libtest.so # 查看是否动态链接成功
ldd main
# linux-vdso.so.1 (0x00007fff596fc000)
# libtest.so => ./libtest.so (0x00007f1d9f61c000)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1d9f200000)
# /lib64/ld-linux-x86-64.so.2 (0x00007f1d9f628000)
// main
#include <test.h> int main()
{
say_hello();
return 0;
}

执行的目标为对 libtest.so 共享库的 malloc 函数进行 hook 操作,替换成我们自定义的一个 my_malloc 实现。

由上述 PLT/GOT 表工作原理 可知, libtest.so 调用 malloc 时会进行重定向操作找到 malloc 的地址进行调用。因此我们只需要更改 got 表中 malloc 的地址指向,指向我们实现的 my_malloc 地址即可实现 hook。

基地址

基于基址的符号偏移地址可以直接通过 readelf -r elf_file 命令查看 .rel.plt 中的信息确定。

我的执行环境的 libtest.so 中的 malloc 偏移地址为 0x4028

~/Documents/ProgramDesign/test_hook> readelf -r libtest.so                                                          

Relocation section '.rela.dyn' at offset 0x4a8 contains 7 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000003e10 000000000008 R_X86_64_RELATIVE 1150
000000003e18 000000000008 R_X86_64_RELATIVE 1110
000000004030 000000000008 R_X86_64_RELATIVE 4030
000000003fe0 000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTM[...] + 0
000000003fe8 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0 000600000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCl[...] + 0
000000003ff8 000700000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0 Relocation section '.rela.plt' at offset 0x550 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000004018 000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000004020 000300000007 R_X86_64_JUMP_SLO 0000000000000000 snprintf@GLIBC_2.2.5 + 0
000000004028 000500000007 R_X86_64_JUMP_SLO 0000000000000000 malloc@GLIBC_2.2.5 + 0
#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include "test.h" #define PAGE_SIZE getpagesize()
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE) void *my_malloc(size_t size)
{
printf("%zu bytes memory are allocated by libtest.so\n", size);
return malloc(size);
} void hook()
{
char line[512];
FILE *fp;
uintptr_t base_addr = 0;
uintptr_t addr; //find base address of libtest.so
if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
while(fgets(line, sizeof(line), fp))
{
if(NULL != strstr(line, "libtest.so") &&
sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
break;
}
fclose(fp);
if(0 == base_addr) return; //the absolute address
addr = base_addr + 0x4028; //add write permission
mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE); //replace the function address
*(void **)addr = my_malloc; //clear instruction cache
__builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
} int main()
{
hook(); say_hello();
return 0;
}

深入浅出 PLT/GOT Hook与原理实践的更多相关文章

  1. Wordpress解析系列之PHP编写hook钩子原理简单实例

    Wordpress作为全球应用最广泛的个人博客建站工具,有很多的技术架构值得我们学习推敲.其中,最著名最经典的编码技术架构就是采用了hook的机制. hook翻译成中文是钩子的意思,单独看这个词我们难 ...

  2. 深入浅出了解OCR识别票据原理(Applying OCR Technology for Receipt Recognition)

    原文:Applying OCR Technology for Receipt Recognition 译文:深入浅出了解OCR识别票据原理 英文票据识别技术, 非中文票据识别技术, 中文情况的ocr更 ...

  3. 深入浅出 Jest 框架的实现原理

    English Version | 中文版 深入浅出 Jest 框架的实现原理 https://github.com/Wscats/jest-tutorial 什么是 Jest Jest 是 Face ...

  4. Android Hook Dexposed原理小析

    dexposed是阿里巴巴在xposed框架上面开发的hotpatch一套框架 当然hotpatch的方式有很多,这里先介绍下dexposed原理 Demo中有个test函数, 在调用hook之前正常 ...

  5. HTTPS中间人攻击实践(原理·实践)

      前言 很早以前看过HTTPS的介绍,并了解过TLS的相关细节,也相信使用HTTPS是相对安全可靠的.直到前段时间在验证https代理通道连接时,搭建了MITM环境,才发现事实并不是我想的那样.由于 ...

  6. Xposed 框架 hook 简介 原理 案例 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  7. JavaScript内部原理实践——真的懂JavaScript吗?(转)

    通过翻译了Dmitry A.Soshnikov的关于ECMAScript-262-3 JavaScript内部原理的文章, 从理论角度对JavaScript中部分特性的内部工作机制有了一定的了解. 但 ...

  8. HOOK相关原理与例子

    消息HOOK 原理: 1. 用户输入消息,消息被放到系统消息队列. 2. 程序发生了某些需要获取输入的事件,就从系统消息队列拿出消息放到程序消息队列中. 3. 应用程序检测到有新的消息进入到程序消息队 ...

  9. 优化技术专题-线程间的高性能消息框架-深入浅出Disruptor的使用和原理

    前提概要 简单回顾 jdk 里的队列: 阻塞队列: ArrayBlockingQueue主要通过:数组(Object[])+ 计数器(count)+ ReetrantLock的Condition (n ...

  10. 北大2022编译原理实践(C/C++)-sysy 编译器构建

    这是今年新推出的实践方案,由往年的sysy->IR1->IR2->RISC V变成了sysy->Koopa->RISC V,通过增量的方式让整个实践过程更容易上手 所以先 ...

随机推荐

  1. 洛谷 P1122 最大子树和 题解

    一道入门的树形DP. 首先我们对于数据进行有序化处理,这便于我们利用数据结构特点(可排序性)来发觉数据性质(有序.单调.子问题等等性质),以便于后续的转化.推理和处理.有序化可以"转化和创造 ...

  2. [nginx]日志中记录自定义请求头

    前言 假设在请求中自定义了一个请求头,key为"version",参数值为"1.2.3",需要在日志中捕获这个请求头. nginx日志配置 只需要用变量http ...

  3. AVR汇编(六):分支指令

    AVR汇编(六):分支指令 分支指令用于改变程序的执行流,分为无条件分支和条件分支两类. 无条件分支指令 JMP JMP 指令用于无条件跳转,类似于C中的 goto 关键字, JMP 指令的跳转范围为 ...

  4. EXP 一款 Java 插件化热插拔框架

    EXP 一款 Java 插件化热插拔框架 前言 多年以来,ToB 的应用程序都面临定制化需求应该怎么搞的问题. 举例,大部分本地化软件厂家,都有一个标准程序,这个程序支持大部分企业的功能需求,但面对世 ...

  5. 9、Spring之代理模式

    9.1.环境搭建 9.1.1.创建module 9.1.2.选择maven 9.1.3.设置module名称和路径 9.1.4.module初始状态 9.1.5.配置打包方式和依赖 <?xml ...

  6. 浅析三维模型OBJ格式轻量化处理常见问题与处理措施

    浅析三维模型OBJ格式轻量化处理常见问题与处理措施 在三维模型OBJ格式轻量化处理过程中,可能会遇到一些问题.以下是一些常见问题以及相应的解决方法: 1.文件大小过大: OBJ格式的三维模型文件通常包 ...

  7. numpy和pandas的基本用法

    安装numpy模块 pip install numpy 可以通过导入numpy模块来使用它 import numpy as np 1.创建数组: a = np.array([1, 2, 3, 4, 5 ...

  8. 通过Scrum实现最大生产力的五种方法

    在数字化.信息化.智能化蓬勃发展的今天,敏捷开发和Scrum已成为重塑项目管理的重要方式. 敏捷是一种体现不同方法的思维方式,包括了Scrum,看板,极限编程(XP).精益开发等众多框架. Scrum ...

  9. windows 网络模拟工具分享

    [下载地址] Releases · jagt/clumsy · GitHub [介绍] 无需安装 无需篡改和代理 系统级限制,不针对单个程序,但可以针对单个IP 离线也可以限制,随停随用 界面简单 [ ...

  10. 「atcoder - agc054c」Roughly Sorted

    link. 高妙题,我只会到构造下界那一步-- 构造下界比较容易,只需要注意到交换一次最多让序列向合法迫近一步即可.则答案下界为 \(\sum_i \max\{\left(\sum_{j < i ...