栈回溯技术arm_v5t_le版

From:韦东山 2007.04.03栈回溯技术及uClibc的堆实现原理.doc

1.    前言

段错误、非法地址访问等问题导致程序崩溃的现象屡屡发生,如果能找到发生错误的函数,往往一眼就能看出BUG所在——对于这类比较简单的问题,比如使用空指针进行读写等,利用栈回溯技术可以很快定位。但是对于数组溢出、内存泄漏等问题导致的程序错误,往往隐藏很深,它们并不当场发作,即使我们一步一步跟踪到发生错误的语句时,也经常会让人觉得“这个地方根本不可能出错啊”——错误在很早以前就隐藏下来了,只不过是这个“不可能出错的语句”触发了它。了解栈的作用、堆的实现,可以让我们脑中对程序的运行、函数的调用、变量的操作有个感官的了解,对解决这类问题会有所帮助。

       关键词:堆栈回溯堆实现栈作用

2.    栈的作用

一个程序包含代码段、数据段、BSS段、堆、栈;其中数据段用来中存储初始值不为0的全局数据,BSS段用来存储初始值为0的全局数据,堆用于动态内存分配,栈用于实现函数调用、存储局部变量。比如对于如下程序:

程序1 section.c

01 #include <stdlib.h>

02 #include <string.h>

03 #include <stdio.h>

04

05 int *g_pBuf;

06 int g_iCount = 10;

07

08 int main(int argc, char **argv)

09 {

10     char str[2];

11     g_pBuf = malloc(g_iCount);

12     printf("Address of main       = 0x%08x\n", (unsigned int)main);

13     printf("Address of g_pBuf     = 0x%08x\n", (unsigned int)&g_pBuf);

14     printf("Address of g_iCount   = 0x%08x\n", (unsigned int)&g_iCount);

15     printf("Address of malloc buf  = 0x%08x\n", (unsigned int)g_pBuf);

16     printf("Address of local buf str  = 0x%08x\n", (unsigned int)str);

17

18     return 0;

19 }

使用如下命令编译得到可执行文件section,反汇编文件section.dis:

arm_v5t_le-gcc  -o  section  section.c -static

arm_v5t_le-objdump -D section > section.dis

在PU S980上的linux环境下,这个程序的输出结果为:

Address of main       = 0x000082b8

Address of g_pBuf     = 0x00082998

Address of g_iCount   = 0x00080bd0

Address of malloc buf  = 0x00083f80

Address of local buf str  = 0xbec80b36

其中main函数的地址为0x000082b8,它处于代码段中;全局变量g_pBuf位于BSS段;全局变量g_iCount位于数据段;使用malloc分配出来的内存地址为0x00083f80,它位于堆中;局部变量str数组的开始地址为0xbec80b36,位于栈中。它们的分布图示如下:

图1 程序各段示意图

栈的作用有二:

①     保存调用者的环境——某些寄存器的值、返回地址

②     存储局部变量

现在通过一个简单的例子来说明栈的作用:

程序2 call.c

01 #include <stdlib.h>

02 #include <string.h>

03 #include <stdio.h>

04

05 void A(int a);

06 void B(int b);

07 void C(int c);

08

09 void A(int a)

10 {

11     printf("%d: A call B\n", a);

12     B(2);

13 }

14

15 void B(int b)

16 {

17     printf("%d: B call C\n", b);

18     C(3);

19 }

20

21 void C(int c)

22 {

23     printf("%d: function C\n", c);

24 }

25

26 int main(int argc, char **argv)

27 {

28     A(1);

29     return 0;

30 }

使用如下命令编译得到可执行文件call,反汇编文件call.dis:

arm_v5t_le-gcc -o  call  call.c  -static

arm_v5t_le-objdump -D call > call.dis

此程序的调用关系为main > A > B > C,现在来看看栈如何变化:

图2 函数调用中栈的变化

上图中,main、A、B、C四个函数的栈大小都是16字节,返回地址都存在栈偏移地址为16的地方。我们是如何知道这点的呢?需要阅读反汇编代码:

……

000082b8 <A>:

82b8:       e92d4800        push    {fp, lr}

82bc:       e28db004        add     fp, sp, #4      ; 0x4

82c0:       e24dd008        sub     sp, sp, #8      ; 0x8

82c4:       e50b0008        str     r0, [fp, #-8]

82c8:       e59f0014        ldr     r0, [pc, #20]   ; 82e4 <A+0x2c>

82cc:       e51b1008        ldr     r1, [fp, #-8]

82d0:       eb0003f8        bl      92b8 <_IO_printf>

82d4:       e3a00002        mov     r0, #2  ; 0x2

82d8:       eb000002        bl      82e8 <B>

82dc:       e24bd004        sub     sp, fp, #4      ; 0x4

82e0:       e8bd8800        pop     {fp, pc}

82e4:       00064584        .word   0x00064584

000082e8 <B>:

82e8:       e92d4800        push    {fp, lr}

82ec:       e28db004        add     fp, sp, #4      ; 0x4

82f0:       e24dd008        sub     sp, sp, #8      ; 0x8

82f4:       e50b0008        str     r0, [fp, #-8]

82f8:       e59f0014        ldr     r0, [pc, #20]   ; 8314 <B+0x2c>

82fc:       e51b1008        ldr     r1, [fp, #-8]

8300:       eb0003ec        bl      92b8 <_IO_printf>

8304:       e3a00003        mov     r0, #3  ; 0x3

8308:       eb000002        bl      8318 <C>

830c:       e24bd004        sub     sp, fp, #4      ; 0x4

8310:       e8bd8800        pop     {fp, pc}

8314:       00064594        .word   0x00064594

00008318 <C>:

8318:       e92d4800        push    {fp, lr}

831c:       e28db004        add     fp, sp, #4      ; 0x4

8320:       e24dd008        sub     sp, sp, #8      ; 0x8

8324:       e50b0008        str     r0, [fp, #-8]

8328:       e59f000c        ldr     r0, [pc, #12]   ; 833c <C+0x24>

832c:       e51b1008        ldr     r1, [fp, #-8]

8330:       eb0003e0        bl      92b8 <_IO_printf>

8334:       e24bd004        sub     sp, fp, #4      ; 0x4

8338:       e8bd8800        pop     {fp, pc}

833c:       000645a4        .word   0x000645a4

00008340 <main>:

8340:       e92d4800        push    {fp, lr}

8344:       e28db004        add     fp, sp, #4      ; 0x4

8348:       e24dd008        sub     sp, sp, #8      ; 0x8

834c:       e50b0008        str     r0, [fp, #-8]

8350:       e50b100c        str     r1, [fp, #-12]

8354:       e3a00001        mov     r0, #1  ; 0x1

8358:       ebffffd6        bl      82b8 <A>

835c:       e3a03000        mov     r3, #0  ; 0x0

8360:       e1a00003        mov     r0, r3

8364:       e24bd004        sub     sp, fp, #4      ; 0x4

8368:       e8bd8800        pop     {fp, pc}

上面红色的指令:

“push    {fp, lr}”表示将fp,lr 寄存器进行压栈,同时 sp – 4×2,即向下移动 8 个字节。

“sub     sp, sp, #8”表示将 sp 寄存器的值减 8,即向下移动 8 个字节。

“pop     {fp, pc}”表示弹出栈顶值到 fp, lr 寄存器,同时 sp+4×2,即向上移动 8 个字节。

局部变量也是存储在栈中,当一个函数的局部变量越多,它的栈越大。

从上图可知:栈中保存着函数的返回地址、局部变量等,那么我们可以从这些返回地址来确定函数的调用关系、调用顺序。这就是下节介绍的栈回溯。

3.    栈回溯

将程序2稍加修改,见程序3第23、24、30、32四行:

程序3 stack.c

01 #include <stdlib.h>

02 #include <string.h>

03 #include <stdio.h>

04

05 void A(int a);

06 void B(int b);

07 void C(int c);

08

09 void A(int a)

10 {

11     printf("%d: A call B\n", a);

12     B(2);

13 }

14

15 void B(int b)

16 {

17     printf("%d: B call C\n", b);

18     C(3);

19 }

20

21 void C(int c)

22 {

23     char *p = (char *)c;

24     *p = ‘A’;

25     printf("%d: function C\n", c);

26 }

27

28 int main(int argc, char **argv)

29 {

30      char a;

31     A(1);

32     C(&a);

33     return 0;

34 }

第23、24两行必然导致段错误而使得程序崩溃,linux内核当发现发生段错误时,会打印出栈信息。我们可以使用栈回溯的方法找到发生错误的原因。

使用如下命令编译得到可执行文件stack,反汇编文件stack.dis:

arm_v5t_le-gcc -g -o stack stack.c -static

arm_v5t_le-objdump -dS stack > stack.dis

运行结果如下(注意:如果你是通过telnet来运行程序,可以使用dmesg命令看到这些栈信息):

/ # ./stack

1: A call B

2: B call C

<2>stack: unhandled page fault (11) at 0x00000003, code 0x817

<1>pgd = c3ca0000

<1>[00000003] *pgd=81b86031, *pte=00000000, *ppte=00000000

<4>

<4>Pid: 662, comm:                stack

<4>CPU: 0

<4>PC is at 0x8338

<4>LR is at 0x830c

<4>pc : [<00008338>]    lr : [<0000830c>]    Not tainted

<4>sp : be897af0  ip : 00000001  fp : be897b04

<4>r10: 00000000  r9 : 00008ac8  r8 : 00008b10

<4>r7 : 00000000  r6 : 00000000  r5 : 00081fe8  r4 : 00000000

<4>r3 : 00000041  r2 : 00000003  r1 : 00000000  r0 : 00000003

<4>Flags: nZCv  IRQs on  FIQs on  Mode USER_32  Segment user

<4>Control: 5317F

<4>Table: 83CA0000  DAC: 00000015

<4>usr stack info

<4>stack mem: sp = be897af0

4> be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 00008354

00004c69 6e757800 000000

<4> be897b24 03be897b 0003be89 000003be 00000003 b4000000 45b40000 0645b400

<4> 000645b4 03000645 00030006 00000300 00000003 14000000 7b140000 897b1400

<4> be897b14 0cbe897b 830cbe89 00830cbe 0000830c a4000083 45a40000 0645a400

……

<4>Backtrace: [<000082fc>] (0x82fc) from [<be897b14>] (0xbe897b14)

<4>Backtrace aborted due to bad frame pointer <000645b4>

<4>Code: e51b3010 e50b3008 e51b2008 e3a03041 (e5c23000)

~ $

上面的蓝色部分

<4>PC is at 0x8338

<4>LR is at 0x830c

表示导致崩溃的指令的地址为0x8338,返回地址为0x830c。一个典型的进程的地址空间为:

~ $ cat /proc/660/maps

00008000-00112000 r-xp 00000000 1f:07 27         /bin/busybox

0011a000-0011b000 rw-p 0010a000 1f:07 27         /bin/busybox

0011b000-001ce000 rwxp 0011b000 00:00 0          [heap]

40000000-40001000 rw-p 40000000 00:00 0

bea24000-bea4e000 rwxp bea24000 00:00 0          [stack]

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x8338 -f -e stack

C

/home/sanya/yjt/bt/stack.c:24

linux-S980:/home/sanya/yjt/bt #

结合此程序的反汇编程序进行回溯:

将PC is at 0x8338所在函数的部分反汇编代码摘录如下:

00008318 <C>:

void C(int c)

{

8318:       e92d4800        push    {fp, lr}

831c:       e28db004        add     fp, sp, #4      ; 0x4

8320:       e24dd010        sub     sp, sp, #16     ; 0x10

8324:       e50b0010        str     r0, [fp, #-16]

char *p = (char *)c;

8328:       e51b3010        ldr     r3, [fp, #-16]

832c:       e50b3008        str     r3, [fp, #-8]

*p = 'A';

8330:       e51b2008        ldr     r2, [fp, #-8]

8334:       e3a03041        mov     r3, #65 ; 0x41

PC->8338:       e5c23000        strb    r3, [r2]

printf("%d: function C\n", c);

833c:       e59f000c        ldr     r0, [pc, #12]   ; 8350 <C+0x38>

8340:       e51b1010        ldr     r1, [fp, #-16]

8344:       eb0003e3        bl      92d8 <_IO_printf>

}

8348:       e24bd004        sub     sp, fp, #4      ; 0x4

834c:       e8bd8800        pop     {fp, pc}

8350:       000645c4        .word   0x000645c4

下面对涉及的每个函数进行分析:

1.  函数C的堆栈:

8318:       e92d4800        push    {fp, lr}

831c:       e28db004        add     fp, sp, #4      ; 0x4

8320:       e24dd010        sub     sp, sp, #16     ; 0x10

be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 54830000

00004c69 6e757800 000000

可知 lr 为 0x0000830c,根据这个地址值,

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x830c -f -e stack

B

/home/sanya/yjt/bt/stack.c:19

linux-S980:/home/sanya/yjt/bt #

在stack.dis中可以再次找到这个地址处于函数B的范围内。

2. 函数B的堆栈:

函数B的汇编指令如下:

void B(int b)

{

82e8:       e92d4800        push    {fp, lr}

82ec:       e28db004        add     fp, sp, #4      ; 0x4

82f0:       e24dd008        sub     sp, sp, #8      ; 0x8

82f4:       e50b0008        str     r0, [fp, #-8]

printf("%d: B call C\n", b);

82f8:       e59f0014        ldr     r0, [pc, #20]   ; 8314 <B+0x2c>

82fc:       e51b1008        ldr     r1, [fp, #-8]

8300:       eb0003f4        bl      92d8 <_IO_printf>

C(3);

8304:       e3a00003        mov     r0, #3  ; 0x3

8308:       eb000002        bl      8318 <C>

}

830c:       e24bd004        sub     sp, fp, #4      ; 0x4

8310:       e8bd8800        pop     {fp, pc}

8314:       000645b4        .word   0x000645b4

82e8:       e92d4800        push    {fp, lr}

82ec:       e28db004        add     fp, sp, #4      ; 0x4

82f0:       e24dd008        sub     sp, sp, #8      ; 0x8

可知函数B的堆栈大小为16字节,函数B执行完后的返回地址 lr 存储在其堆栈偏移地址最外层。函数B栈的的数据紧挨着函数C的栈,取出罗列如下:

be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 54830000

00004c69 6e757800 000000

从上面信息可以知道,函数B的返回地址lr 为 000082dc,根据这个地址值,

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x82dc -f -e stack

A

/home/sanya/yjt/bt/stack.c:13

从stack.dis可知处于函数A的地址范围内。

3. 函数A的堆栈:

函数A的汇编指令如下:

void A(int a)

{

82b8:       e92d4800        push    {fp, lr}

82bc:       e28db004        add     fp, sp, #4      ; 0x4

82c0:       e24dd008        sub     sp, sp, #8      ; 0x8

82c4:       e50b0008        str     r0, [fp, #-8]

printf("%d: A call B\n", a);

82c8:       e59f0014        ldr     r0, [pc, #20]   ; 82e4 <A+0x2c>

82cc:       e51b1008        ldr     r1, [fp, #-8]

82d0:       eb000400        bl      92d8 <_IO_printf>

B(2);

82d4:       e3a00002        mov     r0, #2  ; 0x2

82d8:       eb000002        bl      82e8 <B>

}

82dc:       e24bd004        sub     sp, fp, #4      ; 0x4

82e0:       e8bd8800        pop     {fp, pc}

82e4:       000645a4        .word   0x000645a4

82b8:       e92d4800        push    {fp, lr}

82bc:       e28db004        add     fp, sp, #4      ; 0x4

82c0:       e24dd008        sub     sp, sp, #8      ; 0x8

可知函数A的堆栈大小为16字节,函数B执行完后的返回地址 lr 存储在其堆栈偏移地址最外层。函数A栈的的数据紧挨着函数B的栈,取出罗列如下:

be897b24 03000000 b4450600 03000000 147b89be 0000830c a4450600 02000000

247b89be 000082dc 00000000 01000000 3c7b89be 00008370 647e89be 01000000

00000000 108b0000 00000000 000085b4 00000000 647e89be 01000000 54830000

00004c69 6e757800 000000

从上面信息可以知道,函数A的返回地址为0x00008370,

linux-S980:/home/sanya/yjt/bt # arm_v5t_le-addr2line 0x8370 -f -e stack

main

/home/sanya/yjt/bt/stack.c:32

linux-S980:/home/sanya/yjt/bt #

从stack.dis可知处于函数main的地址范围内。

至此,可以知道main调用A、A调用B、B再调用C时,在函数C中导致程序崩溃。现在认真看一下函数C:

21 void C(int c)

22 {

23     char *p = (char *)c;

24     *p = ‘A’;

25     printf("%d: function C\n", c);

26 }

这个函数太过简单,可以一眼就知道第23、24行代码有问题。但是如果函数C有上千行代码呢?除了睁大眼睛检查C代码外,我们还可以根据它的反汇编码来差错——内核打印出来的栈信息的前面还有些有用的信息:

<2>stack: unhandled page fault (11) at 0x00000003, code 0x817

<1>pgd = c3ca0000

<1>[00000003] *pgd=81b86031, *pte=00000000, *ppte=00000000

<4>

<4>Pid: 662, comm:                stack

<4>CPU: 0

<4>PC is at 0x8338

<4>LR is at 0x830c

<4>pc : [<00008338>]    lr : [<0000830c>]    Not tainted

<4>sp : be897af0  ip : 00000001  fp : be897b04

<4>r10: 00000000  r9 : 00008ac8  r8 : 00008b10

<4>r7 : 00000000  r6 : 00000000  r5 : 00081fe8  r4 : 00000000

<4>r3 : 00000041  r2 : 00000003  r1 : 00000000  r0 : 00000003

<4>Flags: nZCv  IRQs on  FIQs on  Mode USER_32  Segment user

<4>Control: 5317F

现在回过头来看看函数C的反汇编码,从中找出出错的原因:

00008318 <C>:

void C(int c)

{

8318:       e92d4800        push    {fp, lr}

831c:       e28db004        add     fp, sp, #4      ; 0x4

8320:       e24dd010        sub     sp, sp, #16     ; 0x10

8324:       e50b0010        str     r0, [fp, #-16]

char *p = (char *)c;

8328:       e51b3010        ldr     r3, [fp, #-16]

832c:       e50b3008        str     r3, [fp, #-8]

*p = 'A';

8330:       e51b2008        ldr     r2, [fp, #-8]

8334:       e3a03041        mov     r3, #65 ; 0x41

8338:       e5c23000        strb    r3, [r2]

printf("%d: function C\n", c);

833c:       e59f000c        ldr     r0, [pc, #12]   ; 8350 <C+0x38>

8340:       e51b1010        ldr     r1, [fp, #-16]

8344:       eb0003e3        bl      92d8 <_IO_printf>

}

8348:       e24bd004        sub     sp, fp, #4      ; 0x4

834c:       e8bd8800        pop     {fp, pc}

8350:       000645c4        .word   0x000645c4

出错的指令为“8338:       e5c23000        strb    r3, [r2]”,它将寄存器r3的值存到地址(r2)中,只存储1个字节。根据内核打印出来的寄存器值可知r3 : 00000041  r2 : 00000003,写地址为0x03,当然<2>stack: unhandled page fault (11) at 0x00000003, code 0x817

——这不是可写的地址。

阅读汇编代码是件困难的事情,没有其他办法时再用这方法吧。

栈回溯技术arm_v5t_le版相关推荐

  1. 项目经验之谈--驱动崩溃分析之栈回溯技术与反汇编

    1.序言 驱动往往是芯片厂商提供的,而且是不开源的. 一旦崩溃很难查找原因,当然办法是有的,比如内核为此也提供栈回溯技术(低版本的好像没有实现)来定位分析驱动问题.再不济也可以反汇编ko文件. 2.栈 ...

  2. 以SIGSEGV为例详解信号处理(与栈回溯)

    以SIGSEGV为例详解信号处理(与栈回溯) 信号是内核提供的向用户态进程发送信息的机制, 常见的有使用SIGUSR1唤醒用户进程执行子程序或发生段错误时使用SIGSEGV保存用户错误现场. 本文以S ...

  3. AI战“疫“之路:​揭秘高精准无感测温系统的全栈AI 技术

    在这个全民抗疫的特殊时期,今年的春节返潮来得比往年迟了许多.如今不少企业结束了远程办公,开始陆续复工,一时间,无论是重点防控的机场.火车站,还是学校.企业.社区等密集型场所,都安排了密集的防疫驻扎点. ...

  4. 多场景下的AI疫情防控“天网”:解读云边端联动下的全栈AI技术

    在全民抗疫的特殊时期下,伴随着春运返潮,企业陆续复工,从重点防控的机场.火车站等场所,到学校.企业.社区等密集型场所,都是不能忽视的地点.除了人工逐一测量体温排查外,我们还发现,在人员复杂.流动量大地 ...

  5. 2009年9月三级网络技术51CTO版考前压轴试题

    2009年9月三级网络技术51CTO版考前压轴试题<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:o ...

  6. Linux内核出错的栈打印详解,linux内核中打印栈回溯信息 - dump_stack()函数分析

    简介 当内核出现比较严重的错误时,例如发生Oops错误或者内核认为系统运行状态异常,内核就会打印出当前进程的栈回溯信息,其中包含当前执行代码的位置以及相邻的指令.产生错误的原因.关键寄存器的值以及函数 ...

  7. 深入浅出根据函数调用过程谈栈回溯原理

                通过分析函数调用过程的堆栈变化,可以看出在被调函数的EBP寄存器地址存放的是调用函数的EBP寄存器地址,EBP地址+4存放的是函数调用完成后的下一条指令存放地址,该指令的前一条 ...

  8. 多场景下的AI疫情防控“天网”:解读云边端联动下的全栈 AI 技术

    在全民抗疫的特殊时期下,伴随着春运返潮,企业陆续复工,从重点防控的机场.火车站,到学校.企业.社区等密集型场所,都是不能忽视的地点.除了人工逐一测量体温排查外,我们还发现,在人员复杂.流动量大地方的出 ...

  9. 深度linux系统gho,深度技术ghost版的win7操作系统下载

    很多朋友都想要深度技术ghost版的win7操作系统下载,因为深度技术ghost版的win7操作系统下载还是不错的,在业内的评价蛮高.可是我们到底要如何深度技术ghost版的win7操作系统下载呢?别 ...

最新文章

  1. Canvas VS . SVG
  2. Linux 文件系统引起的云盘文件系统异常导致 MySQL 数据页损坏事故恢复复盘
  3. 《关键对话》读书笔记作文3700字
  4. linux线程同步(3)-读写锁
  5. linux 7 zip软件下载,linux安装使用7zip教程
  6. STM32F103ZET6 蜂鸣器、按键
  7. 【ArcGIS微课1000例】0016:ArcGIS书签操作(添加书签、管理书签)知多少?
  8. mysql索引原理传送门_MySQL索引底层实现原理
  9. 小米申请雷军签名商标获批
  10. 基本的python内置函数str(x)_Python内置函数(61)——str
  11. HDU 1068 Girls and Boys(最大独立集合 = 顶点数 - 最大匹配数)
  12. mysql get global_getdata table表格数据join mysql方法
  13. jupyter notbook远程连接配置(Ubuntu16.04)
  14. python 遍历字符串_python中如何实现遍历字符串的方法
  15. Win10系统蓝屏错误DPC怎么解决【系统天地】
  16. GeekTool使用cal格式不对齐
  17. 案例介绍ps去除网纹的方法
  18. maplibre显示经纬线,(动态若干条)
  19. python七巧板拼图代码_为什么代码都是用英文来写的,将来会有用中文写代码的那天吗?...
  20. 【历史上的今天】12 月 16 日:晶体管问世;IBM 停售 OS/2;科幻小说巨匠诞生

热门文章

  1. Mysql的distinct、order by和group by冲突报错
  2. uni-app 实现用户点击后自动拨打指定电话号码
  3. 微信环境中无法下载APP的解决方案
  4. uniapp小程序中使用base64格式的字体图标iconfont的详细步骤
  5. 润滑油市场现状研究分析与发展前景预测报告
  6. 2021-2027全球及中国油润滑压缩机行业研究及十四五规划分析报告
  7. Python字典的遍历,包括key遍历/value遍历/item遍历/
  8. 【Laravel3.0.0源码阅读分析】文件类file.php
  9. Wampserver教程
  10. apache jserv漏洞Apache漏洞