(돌려) 정적 분석 및 동적 라이브러리

DLL 라이브러리와 정적 라이브러리는 프로그래밍에서 아주 흔한 일이지만 같은 일반적인 것들 나는 그것이 너무 많은 출입구가있는 것을 발견 한 후 다시 골프장 진짜 의도를 알 수. 이 문서는 리눅스 플랫폼의 차세대, 정적 라이브러리 관계에 따라 DLL 생성, 특히 GCC 컴파일러를 설명하고, 심지어 인기있는 컴파일러 도구 사용의 물결을 확장합니다. 거기가 알고있는 때 볼 수있는 일부 콘텐츠입니다,하지만 난 약속하지만, 모르는 이렇게 몇몇 하드 코어 소위가있을 것입니다.

도서관의 목적

보기의 거친 점의 측면에서 표면에서 라이브러리의 유용성은 코드 재사용을 촉진하고 모든 노하우 하를 해제하는 것입니다,하지만 난 우리가 깊이 물건이 말을해야한다고 생각합니다.

일반적인 부품을 많이 사용하는 실행 프로그램 코드의 수가 많은 경우 라이브러리 정적 라이브러리와 동적 라이브러리가로드 및 매핑을 수행하는 데 사용되는 동적 라이브러리 포인트 (당신은 알고있다), 다음 코드의 일부를 넣을 수 있습니다 독립 동적 라이브러리로,이 약간의 속도,하지만 메모리 사용량을 줄이기 위해 큰 도움을 추가 있지만. 그리고 라이브러리 버전 업그레이드의 문제를 해결할 수 있습니다, 우리는 좋은 계약 API 인터페이스를 가지고, 당신은 기능을 업그레이드해야하거나 당신이 할 수있는,에 이동하지 않고 모든 시간 동적 라이브러리 업데이트 - 버그 해제 할 때까지 대기, 자신의 프로그램 내부에서 이러한 인터페이스를 호출 코드의 비즈니스 로직, Glibc의입니다.

두 가지 주요 목적에 동적 라이브러리를 체결하려면 :

  1. 볼륨 절감, 코드 재사용의 증가 속도.
  2. 쉬운, 버전 관리, 유지 업그레이드합니다.

동적 라이브러리 기능 :

  1. 로딩 동작시, 높은 다중화 동작을 제공한다.
  2. 너무 많은 양의 실행 파일 자체를지고 링크를 다시 컴파일하지 않고 업그레이드 실행 파일 후, 즉 학생들이 사용할 준비하지 마십시오.

정적로드 라이브러리가 실행되지 않을 것이다,이 같은 반복 다중화 기능이 존재하지 않는 것, 그것은 재사용 높은 코드 패키지의 모음입니다,하지만 그것은 단지 쓰기로 컴파일 시간 재사용 코드를 제공합니다 로드 프로그램을 제공하지 않고, 재사용 시간을 실행합니다. 정적 라이브러리는 실행 파일 내부에 컴파일 심볼 테이블의 직접적인 카피 기능, 그것은 또한 크기가 훨씬 더 큰 실행 파일이 될 것입니다 및 런타임 재사용이 깨진 것을 의미한다. 그러나 런타임 실행 파일에 추가 라이브러리를로드하지 않고, 우리는 단지 몇 개의 파일을 복사 할 필요가 물리적으로 다른 시스템에서 실행 파일을 전송할 수 있다는 장점이 있습니다 때.

정적 라이브러리의 목적 :

  1. 제공하는 코드를 컴파일 타임 코드 재사용.
  2. 의존 실행 파일을 줄이고, 파일을 복사하는 몇 사본이 필요합니다.

정적 라이브러리 기능 :

  1. 그것은, 그러나 추가 라이브러리에 의존하지 않고 재사용 런타임을 훼손, 실제 조각이 포함되어 있기 때문에 그 결과 실행 파일 크기가 큽니다.
  2. 수집 대상 파일 자체는 대상 파일 이외의 추가 정보를 제공하지 않습니다.
  3. 당신이 실행 파일을 다시 컴파일해야 업그레이드 할 때 동적 라이브러리 쉽게 종속성으로 업그레이드 프로세스를 비교했다.

분해 라이브러리

정적 라이브러리를 만들어 전체 .o 인 객체 등 심볼 테이블 정보, 내부 동적 링크 라이브러리를 포함하지 않는, 하나 개의 구성에 의해 하나의 파일, 동적 라이브러리 물건의 내부를 명확하게 정적 라이브러리보다 더 복잡하다, 동적 라이브러리 로더가 필요한 크기 때문에, 심볼 테이블의 위치 정보, 기호 의존성 등, 나는 예를 들어, 자체 내장 된 동적 및 정적 라이브러리를 선택하려면 여기입니다.

1
4
5
6
(7)
8
9
(10)
(11)
(12)
(13)
(14)
(15)
(16)
(17)
(18)
(19)
(20)
(21)
(22)
(23)
(24)
(25)
(26)
(27)
(28)
(29)
(30)
(31)
(32)
X @ 우분투 : ~ / 워크 스테이션 / 응용 프로그램 / 컴파일 / libs와 $ readelf -d libdylib1.so
 
오프셋 0xf00에서 동적 섹션은 24 항목이 포함되어 있습니다
태그 유형 이름 / 값
0x00000001 (필요한) 공유 라이브러리 : [libc.so.6으로]
0x0000000c (INIT) 0x3c4
0x0000000d (FINISH) 0x5b0
0x00000019 (INIT_ARRAY) 0x1ef4
0x0000001b (INIT_ARRAYSZ) 4 (바이트)
0x0000001a (FINI_ARRAY) 0x1ef8
0x0000001c (FINI_ARRAYSZ) 4 (바이트)
0x6ffffef5 (GNU_HASH) 0x138
0x00000005 (STRTAB) 0x258
0x00000006 (SYMTAB) 0x178
0x0000000A 오류 (STRSZ) 198 (바이트)
0x0000000b (SYMENT) 16 (바이트)
0x00000003 (PLTGOT) 0x1ff4
0x00000002 (PLTRELSZ) 24 (바이트)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x3ac
0x00000011 (REL) 0x36c
0x00000012 (RELSZ) 64 (바이트)
0x00000013 (누그러) 8 (바이트)
0x6ffffffe (VERNEED) 0x33c
0x6fffffff (VERNEEDNUM 1)
0x6ffffff0 (VERSYM) 0x31e
0x6ffffffa (RELCOUNT) 3
을 0x00000000 (NULL)이 0x0
X @ 우분투 : ~ / 워크 스테이션 / 응용 프로그램 / 컴파일 / libs와 $ nm의 --size-종류 -r libdylib1.so
00000029 T의 dy1_print
00000029 T의 dy1_clean
00000001 ㄴ completed.6874

내 동적 라이브러리에만 두 가지 기능 : dy1_printdy1_clean. 내가 링크 옵션은 컴파일하는 데 사용 : -fPIC --shared. NM 명령은 각각 두 개의 글로벌 코드 세그먼트가 있음을 알 수있다  dy1_printdy1_clean볼 수 readelf를 사용하는 의존 동적 라이브러리  libc.so.6 이 라이브러리를 동적.

여기에 정보의 정적 라이브러리는 다음과 같습니다

1
4
5
6
(7)
8
9
(10)
X @ 우분투 : ~ / 워크 스테이션 / 응용 프로그램 / 컴파일 / libs와 $ readelf -d libstlib1.a
 
파일 : libstlib1.a (stlib1.o)
X @ 우분투 : ~ / 워크 스테이션 / 응용 프로그램 / 컴파일 / libs와 $ nm의 --size-종류 -r libstlib1.a
 
stlib1.o :
00000029 T의 st1_print
00000029 T st1_clean
X@ubuntu:~/workstation/apps/compiles/Libs$ ar -t libstlib1.a
stlib1.o

 

可以看到使用 readelf 完全看不出来静态库的依赖什么的,只有一个 File,它代表该静态库是由 stlib1.o 这个目标文件组成。nm 可以看到符号表有 st1_printst1_clean。从这里也可以看出,静态库单纯就是把目标文件打包在一块,避免每次链接时候需要写一大堆文件文件的尴尬,除此之外,它与单纯的多个 .o 目标文件组合链接生成可执行文件没有任何区别。

库与库之间的依赖

库与库之间的依赖可是非常大的一个知识点儿,与其说是知识点,不如说是坑点,不知道其它公司项目的库与库之间的依赖关系是如何,就我自己接触到的稍微大点的项目,库与库之间的依赖关系简直是一团乱麻,稍有不慎编译的时候就万劫不复。

动态库依赖动态库

我在我的本地环境构造了几个动态库,其中动态库1(libdylib1.so)里面包含 dy1_printdy1_clean 两个符号,动态库2(libdylib2.so)里面包含 dy2_printdy2_clean 两个符号,其中 dy2_clean 调用到了动态库1里面的 dy1_clean。下面我按照这样的方法生成动态库与可执行文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gcc -o libdylib1.so -fPIC --shared dylib1.c
gcc -o libdylib2.so -fPIC --shared dylib2.c
X@ubuntu:~/workstation/apps/compiles/Libs$ cat main.c
#include <stdio.h>
int main(int argc, char *argv[])
{
dy2_clean();
return 0;
}
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -fPIC -L. -ldylib2
./libdylib2.so: undefined reference to `dy1_clean'
collect2: error: ld returned 1 exit status
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -fPIC -L. -ldylib1 -ldylib2
./libdylib2.so: undefined reference to `dy1_clean'
collect2: error: ld returned 1 exit status
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -L. -ldylib2 -ldylib1
X@ubuntu:~/workstation/apps/compiles/Libs$ export LD_LIBRARY_PATH=~/workstation/apps/compiles/Libs
X@ubuntu:~/workstation/apps/compiles/Libs$ ./main
This is dylib2's clean funciton.
This is dylib1's clean funciton.

可以看到只有最后一种编译链接方式可以成功编译、生成、运行程序,可以看到如果动态库之间有相互依赖的话在最终链接可执行文件的时候需要把被依赖项放到后面,否则就会提示找不到某某符号,这里也可以看出其依赖解析关系是从前往后的,前面的动态库会往后面找依赖项。

采用这种方式编译出来的库与可执行文件使用 readelf 查看得到依赖关系如下(隐藏部分不关注信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libdylib1.so
 
Dynamic section at offset 0xf00 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libdylib2.so
 
Dynamic section at offset 0xf00 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d main
 
Dynamic section at offset 0xef8 contains 26 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdylib2.so]
0x00000001 (NEEDED) Shared library: [libdylib1.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -s libdylib2.so
 
Symbol table '.dynsym' contains 15 entries:
7: 00000000 0 NOTYPE GLOBAL DEFAULT UND dy1_clean

 

可以看到在库层级使用这种方式是看不出来真实的依赖关系的,也就是 libdylib2.so->libdylib1.so 这一层依赖,从这里也可以猜到在动态库生成的时候是没有链接这一步骤的,否则肯定会出现错误提示,并且使用 readelf -s 可以看到动态库2里面的 dy1_clean 属于未定义符号,总结如下:

  1. 动态库生成的时候没有链接动作,并且默认允许未定义符号的存在。
  2. 动态库的依赖关系是从前往后解析的,被依赖者需要放在使用者的前面。
  3. 可执行文件需要解析最终的真实依赖关系,因此必须把所有的动态库全部链接进来。

上面的方式有某些弊端,那就是如果我是直接拿到 [libdylib2.so],[libdylib1.so] 两个成品动态库的话,不加以分析我是不知道它们两个之间的依赖关系的,这种情况下如果我没有更加详细的文档的话我是不知道如何去链接这些动态库的,这个时候可以采用下面的方式进行动态库的生成以及可执行文件的链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o libdylib2.so -fPIC --shared -Wl,--no-undefined dylib2.c
/tmp/ccrLpPaL.o: In function `dy2_clean':
dylib2.c:(.text+0x4e): undefined reference to `dy1_clean'
collect2: error: ld returned 1 exit status
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o libdylib2.so -fPIC --shared -Wl,--no-undefined dylib2.c -L. -ldylib1
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -L. -ldylib2
yellow@ubuntu:~/workstation/apps/compiles/Libs$ ./main
This is dylib2's clean funciton.
This is dylib1's clean funciton.
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libdylib2.so
 
Dynamic section at offset 0xef8 contains 25 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdylib1.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d main
 
Dynamic section at offset 0xf00 contains 25 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdylib2.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]

 

秘诀在于 -Wl,--no-undefined 这个链接选项,它要求在生成动态库的时候不能有未定义的符号,这个选项可以帮助我们检查动态库之间的依赖关系,最终使用 -l 链接生成动态库之后可以看到器依赖项里面包含了 libdylib1.so,这个时候在生成可执行文件的时候就无需再指定链接多个动态库了,只需要指定链接一个 libdylib2.so 即可,剩下的链接工作编译器自己会去做的。

为什么编译的时候不默认使用 -Wl,--no-undefined 选项呢,这个在后面的库于库之间相互依赖这节会说明。

动态库依赖静态库

动态库依赖静态库是十分不推荐的,因为这违背了动态库的初心,这里就不再去重复做试验了,直接说结果,如果动态库里面有调用到某个静态库里面的函数,并且在生成动态库的时候没有去做 -Wl,--no-undefined 的限定动作,那么在生成可执行文件的时候无论如何都是无法成功生成的。

况且,思考一下,这种依赖关系本身就是畸形的,动态库依赖动态库还是可以理解的,它们都是动态库,种类相同,个性相似,功效雷同。但是动态库一旦依赖了静态库,这就变态了,因为它们的目的不同,动态库本质上是为了运行时动态加载,而静态库则是完全拷贝代码段,这两者的属性是相互排斥的,一旦出现这种依赖关系,动态库还怎么动态加载!!!如果允许这种行为的话,动态库就失去了动态库的意义,从设计哲学上来讲就是不应该允许的。

如果无法避免出现这种情况,那就加上 -Wl,--no-undefined 这个链接选项,这样静态库里面的被依赖代码段就会被拷贝到动态库里面成为动态库的一部分,虽然体积会变大,但是完全不影响它的功能与初心。

静态库依赖静态库

关注静态库的时候,就要把它一眼看透,不要关注它的表面,而要关注它的内在,外在衣物不重要,重要的是里面的东西,那才是本质。那么静态库的本质就是一个个的 .o 目标文件的集合,既然是一个集合,就完全可以按照我们常规理解的 .o 文件之间的相互依赖来进行解析。

静态库依赖静态库就需要按照库的解析顺序,上文说过,需要把被依赖者放在使用者的后面,仅此而已。静态库的生成使用类似 ar -cr libstlibname.a x.o xx.o xxx.o 的命令来生成,它是完全没有链接的过程的,也就是不会有上面的 -Wl,--no-undefined 链接选项可用,在最终生成可执行文件的时候必须得人为解析、指定依赖关系,并且遵循一定的依赖先后顺序。

静态库依赖动态库

静态库依赖动态库与普通目标文件的依赖没有二致,也是需要在链接的时候把相关的动态库放在这个静态库的后面,这样就能完成正确的链接过程。

那么为什么静态库就可以依赖动态库而又不至于变态呢,因为静态库的本质是目标文件的合集,你可以完全把它当做是 main.c 文件的一部分,把左右的目标文件看作一个整体,这样就可以想象得到为什么静态库可以依赖动态库了,本质上它与 main.c 文件依赖动态库是一样的性质。

循环依赖

循环依赖就是库 A 依赖库 B,同时库 B 又依赖库 A,完全就是鸡生蛋、蛋生鸡,虽然在实际的开发过程当中不推荐这种依赖关系,但是有时候又会不可避免的出现这种依赖关系,本质上也很难说到底是不是设计缺陷,但是事实是这种情况是会发生的。

有循环依赖的时候上面有几个点就失效了:

  1. 不能使用 -Wl,--no-undefined 来生成动态库,因为相互依赖问题无法在生成动态库的时候解决。
  2. 生成可执行文件的时候依赖顺序规则失效,不管两个库谁在先谁在后都无法完成链接过程。

我构造两个动态库,libdylib1.so 里面调用 libdylib2.so 里面的函数,libdylib2.so 调用 libdylib1.so 里面的函数,形成相濡以沫的关系,接下来我会在 main.c 里面调用两者里面的一个函数。这样就形成了相互依赖的关系。

相互依赖的关系可以在链接的时候写两遍库来解决,比如:

1
gcc -o main main.c -L. -ldylib1 -ldylib2 -ldylib1 -ldylib2

 

也可以使用:

1
gcc -o main main.c -L. -Wl,--start-group -ldylib1 -ldylib2 -Wl,--end-group

 

不过我发现在一些高版本的编译器中,比如我使用的 gcc-4.9 里面就不用加这些额外的选项,貌似它会自行去解决这些循环依赖关系的。

库的加载

静态库全部都是运行前加载的,在链接时候就全部导入到可执行文件里面的,这个就不说了。动态库有两种加载方式,一个是运行前加载,一个是运行时加载。

  1. 运行前加载
    运行前加载意思就是在程序被执行的时候,在 main 函数之前程序会先去加载它链接时候指定的动态库,等准备好之后才会跑到 main 函数处执行。
  2. 运行时加载
    运行时加载就需要依靠 dlopen、dlsym、dlclose 这些动态库辅助加载函数来完成。这类动态库无需在函数 main 函数之前完成加载,而是在程序里面随用随加载。

使用 strace 跟踪一个可执行文件的运行前加载动态库状况如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
X@ubuntu:~/workstation/apps/compiles/Libs$ strace ./main
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/libdylib1.so", O_RDONLY|O_CLOEXEC) = 3
open("/home/yellow/workstation/apps/compiles/Libs/libdylib2.so", O_RDONLY|O_CLOEXEC) = 3
open("/home/yellow/workstation/apps/compiles/Libs/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

 

可以看到它找动态库的路径是极为冗长的,这是跟 ld.so 程序的一个特性有关的,具体的特性是一个动态库版本查找规则,这里就不深入去说了,这个冗长的步骤是无法去除的,也就是无法一步到位,因为这是个特性哈,知道有这个过程就行,并且在 ld.so.cache 文件里面可以自定义程序的库路径查找逻辑。

拓展

  1. 动态库的裁剪
    可以通过 readelf -d 来分析动态库的依赖关系,可执行文件的依赖关系来确认是否有用不到的动态库,如果用不到,删除之,当然要注意添加白名单,主要是照顾那些运行时使用 dl 库函数加载的动态库。
  2. 静态库打包到动态库里面
    有时候我们可能需要把一个静态库成品转化为一个动态库,就可以使用:
    1
    gcc -shared -o libdylib1.so -L. -Wl,--whole-archive libstlib1.a -Wl,--no-whole-archive

这个会把静态库全部打包进动态库,使用 readelf -s 可以看到动态库里面多了一些符号。

    1. 动态库的预加载
      写一个空的 main 函数,只包含一个 hello world,但是链接的时候添加想要预加载的动态库,就可以使用该袖珍版程序提前把动态库加载到内存里面,在真正的可执行程序运行的时候这个动态库的家在过程就快很多了。通常这个特性会用在嵌入式设备的快速启动优化当中,不细讲了,仅抛砖引玉。
    2. 符号表大小分析
      使用 nm 命令可以分析 elf 文件里面的符号表,特别关注其大小信息就可以使用 nm --size-sort -r elf,这个可以把符号表由大到小排列,用于裁剪程序的体积。
    3. 不加载未使用的函数
      在静态库链接的时候可能会有很多未使用到的函数、变量等等,可以使用 -fdata-sections-ffunciton-sections 等选项来去掉那些用不到的函数,这里去掉只是在最终的可执行文件里面看不到而已,并不是从静态库里面删掉。

추천

출처www.cnblogs.com/Spider-spiders/p/11699266.html