ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

关于ptrace附加到多线程的进程

2021-09-18 16:01:51  阅读:327  来源: 互联网

标签:errno counter pid 附加 线程 ptrace stderr fprintf 多线程


在研究此问题时,我遇到了使用ptrace的可能想法,但是我无法正确理解ptrace与线程的交互方式。

假设我有一个给定的多线程主进程,并且我想附加到其中的特定线程(可能来自派生的子进程)。

  • 我可以附加到特定线程吗? (有关此问题的手册有所不同。)

  • 如果是这样,是否意味着单步执行仅一步步执行该线程的指令?它会停止所有进程的线程吗?

  • 如果是这样,我调用PTRACE_SYSCALL或PTRACE_SINGLESTEP时所有其他线程是否仍保持停止状态,还是所有线程都继续?有没有一种方法可以只在一个线程中前进,但要确保其他线程保持停止状态?

基本上,我想通过强制所有线程停止来同步原始程序,然后仅通过单步执行一个被跟踪的线程来执行一小套单线程指令。

到目前为止,我的个人尝试看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

pid_t target = syscall(SYS_gettid);   // get the calling thread's ID
pid_t pid = fork();

if (pid > 0)
{
    waitpid(pid, NULL, 0);            // synchronise main process

    important_instruction();
}
else if (pid == 0)
{
    ptrace(target, PTRACE_ATTACH, NULL, NULL);    // does this work?

    // cancel parent's"waitpid" call, e.g. with a signal

    // single-step to execute"important_instruction()" above

   ptrace(target, PTRACE_DETACH, NULL, NULL);     // parent's threads resume?

   _Exit(0);
}

但是,我不确定,也找不到合适的引用,因为这是并发正确的,并且保证important_instruction()仅在所有其他线程停止时才执行。我也了解,当父母从其他地方接收到信号时,可能会出现竞争状况,而且我听说应该改用PTRACE_SEIZE,但这似乎并不存在。

任何澄清或参考将不胜感激!


我写了第二个测试用例。我不得不添加一个单独的答案,因为它太长了,无法放入包含示例输出的第一个答案。

好。

首先,这是tracer.c:

好。

1
2
3
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370

#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <dirent.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <stdio.h>
#ifndef   SINGLESTEPS
#define   SINGLESTEPS 10
#endif

/* Similar to getline(), except gets process pid task IDs.
 * Returns positive (number of TIDs in list) if success,
 * otherwise 0 with errno set. */
size_t get_tids(pid_t **const listptr, size_t *const sizeptr, const pid_t pid)
{
    char     dirname[64];
    DIR     *dir;
    pid_t   *list;
    size_t   size, used = 0;

    if (!listptr || !sizeptr || pid < (pid_t)1) {
        errno = EINVAL;
        return (size_t)0;
    }

    if (*sizeptr > 0) {
        list = *listptr;
        size = *sizeptr;
    } else {
        list = *listptr = NULL;
        size = *sizeptr = 0;
    }

    if (snprintf(dirname, sizeof dirname,"/proc/%d/task/", (int)pid) >= (int)sizeof dirname) {
        errno = ENOTSUP;
        return (size_t)0;
    }

    dir = opendir(dirname);
    if (!dir) {
        errno = ESRCH;
        return (size_t)0;
    }

    while (1) {
        struct dirent *ent;
        int            value;
        char           dummy;

        errno = 0;
        ent = readdir(dir);
        if (!ent)
            break;

        /* Parse TIDs. Ignore non-numeric entries. */
        if (sscanf(ent->d_name,"%d%c", &value, &dummy) != 1)
            continue;

        /* Ignore obviously invalid entries. */
        if (value < 1)
            continue;

        /* Make sure there is room for another TID. */
        if (used >= size) {
            size = (used | 127) + 128;
            list = realloc(list, size * sizeof list[0]);
            if (!list) {
                closedir(dir);
                errno = ENOMEM;
                return (size_t)0;
            }
            *listptr = list;
            *sizeptr = size;
        }

        /* Add to list. */
        list[used++] = (pid_t)value;
    }
    if (errno) {
        const int saved_errno = errno;
        closedir(dir);
        errno = saved_errno;
        return (size_t)0;
    }
    if (closedir(dir)) {
        errno = EIO;
        return (size_t)0;
    }

    /* None? */
    if (used < 1) {
        errno = ESRCH;
        return (size_t)0;
    }

    /* Make sure there is room for a terminating (pid_t)0. */
    if (used >= size) {
        size = used + 1;
        list = realloc(list, size * sizeof list[0]);
        if (!list) {
            errno = ENOMEM;
            return (size_t)0;
        }
        *listptr = list;
        *sizeptr = size;
    }

    /* Terminate list; done. */
    list[used] = (pid_t)0;
    errno = 0;
    return used;
}


static int wait_process(const pid_t pid, int *const statusptr)
{
    int   status;
    pid_t p;

    do {
        status = 0;
        p = waitpid(pid, &status, WUNTRACED | WCONTINUED);
    } while (p == (pid_t)-1 && errno == EINTR);
    if (p != pid)
        return errno = ESRCH;

    if (statusptr)
        *statusptr = status;

    return errno = 0;
}

static int continue_process(const pid_t pid, int *const statusptr)
{
    int   status;
    pid_t p;

    do {

        if (kill(pid, SIGCONT) == -1)
            return errno = ESRCH;

        do {
            status = 0;
            p = waitpid(pid, &status, WUNTRACED | WCONTINUED);
        } while (p == (pid_t)-1 && errno == EINTR);

        if (p != pid)
            return errno = ESRCH;

    } while (WIFSTOPPED(status));

    if (statusptr)
        *statusptr = status;

    return errno = 0;
}

void show_registers(FILE *const out, pid_t tid, const char *const note)
{
    struct user_regs_struct regs;
    long                    r;

    do {
        r = ptrace(PTRACE_GETREGS, tid, &regs, &regs);
    } while (r == -1L && errno == ESRCH);
    if (r == -1L)
        return;

#if (defined(__x86_64__) || defined(__i386__)) && __WORDSIZE == 64
    if (note && *note)
        fprintf(out,"Task %d: RIP=0x%016lx, RSP=0x%016lx. %s
", (int)tid, regs.rip, regs.rsp, note);
    else
        fprintf(out,"Task %d: RIP=0x%016lx, RSP=0x%016lx.
", (int)tid, regs.rip, regs.rsp);
#elif (defined(__x86_64__) || defined(__i386__)) && __WORDSIZE == 32
    if (note && *note)
        fprintf(out,"Task %d: EIP=0x%08lx, ESP=0x%08lx. %s
", (int)tid, regs.eip, regs.esp, note);
    else
        fprintf(out,"Task %d: EIP=0x%08lx, ESP=0x%08lx.
", (int)tid, regs.eip, regs.esp);
#endif
}


int main(int argc, char *argv[])
{
    pid_t *tid = 0;
    size_t tids = 0;
    size_t tids_max = 0;
    size_t t, s;
    long   r;

    pid_t child;
    int   status;

    if (argc < 2 || !strcmp(argv[1],"-h") || !strcmp(argv[1],"--help")) {
        fprintf(stderr,"
");
        fprintf(stderr,"Usage: %s [ -h | --help ]
", argv[0]);
        fprintf(stderr,"       %s COMMAND [ ARGS ... ]
", argv[0]);
        fprintf(stderr,"
");
        fprintf(stderr,"This program executes COMMAND in a child process,
");
        fprintf(stderr,"and waits for it to stop (via a SIGSTOP signal).
");
        fprintf(stderr,"When that occurs, the register state of each thread
");
        fprintf(stderr,"is dumped to standard output, then the child process
");
        fprintf(stderr,"is sent a SIGCONT signal.
");
        fprintf(stderr,"
");
        return 1;
    }

    child = fork();
    if (child == (pid_t)-1) {
        fprintf(stderr,"fork() failed: %s.
", strerror(errno));
        return 1;
    }

    if (!child) {
        prctl(PR_SET_DUMPABLE, (long)1);
        prctl(PR_SET_PTRACER, (long)getppid());
        fflush(stdout);
        fflush(stderr);
        execvp(argv[1], argv + 1);
        fprintf(stderr,"%s: %s.
", argv[1], strerror(errno));
        return 127;
    }

    fprintf(stderr,"Tracer: Waiting for child (pid %d) events.

", (int)child);
    fflush(stderr);

    while (1) {

        /* Wait for a child event. */
        if (wait_process(child, &status))
            break;

        /* Exited? */
        if (WIFEXITED(status) || WIFSIGNALED(status)) {
            errno = 0;
            break;
        }

        /* At this point, only stopped events are interesting. */
        if (!WIFSTOPPED(status))
            continue;

        /* Obtain task IDs. */
        tids = get_tids(&tid, &tids_max, child);
        if (!tids)
            break;

        printf("Process %d has %d tasks,", (int)child, (int)tids);
        fflush(stdout);

        /* Attach to all tasks. */
        for (t = 0; t < tids; t++) {
            do {
                r = ptrace(PTRACE_ATTACH, tid[t], (void *)0, (void *)0);
            } while (r == -1L && (errno == EBUSY || errno == EFAULT || errno == ESRCH));
            if (r == -1L) {
                const int saved_errno = errno;
                while (t-->0)
                    do {
                        r = ptrace(PTRACE_DETACH, tid[t], (void *)0, (void *)0);
                    } while (r == -1L && (errno == EBUSY || errno == EFAULT || errno == ESRCH));
                tids = 0;
                errno = saved_errno;
                break;
            }
        }
        if (!tids) {
            const int saved_errno = errno;
            if (continue_process(child, &status))
                break;
            printf(" failed to attach (%s).
", strerror(saved_errno));
            fflush(stdout);
            if (WIFCONTINUED(status))
                continue;
            errno = 0;
            break;
        }

        printf(" attached to all.

");
        fflush(stdout);

        /* Dump the registers of each task. */
        for (t = 0; t < tids; t++)
            show_registers(stdout, tid[t],"");
        printf("
");
        fflush(stdout);

        for (s = 0; s < SINGLESTEPS; s++) {
            do {
                r = ptrace(PTRACE_SINGLESTEP, tid[tids-1], (void *)0, (void *)0);
            } while (r == -1L && errno == ESRCH);
            if (!r) {
                for (t = 0; t < tids - 1; t++)
                    show_registers(stdout, tid[t],"");
                show_registers(stdout, tid[tids-1],"Advanced by one step.");
                printf("
");
                fflush(stdout);
            } else {
                fprintf(stderr,"Single-step failed: %s.
", strerror(errno));
                fflush(stderr);
            }
        }

        /* Detach from all tasks. */
        for (t = 0; t < tids; t++)
            do {
                r = ptrace(PTRACE_DETACH, tid[t], (void *)0, (void *)0);
            } while (r == -1 && (errno == EBUSY || errno == EFAULT || errno == ESRCH));
        tids = 0;
        if (continue_process(child, &status))
            break;
        if (WIFCONTINUED(status)) {
            printf("Detached. Waiting for new stop events.

");
            fflush(stdout);
            continue;
        }
        errno = 0;
        break;
    }
    if (errno)
        fprintf(stderr,"Tracer: Child lost (%s)
", strerror(errno));
    else
    if (WIFEXITED(status))
        fprintf(stderr,"Tracer: Child exited (%d)
", WEXITSTATUS(status));
    else
    if (WIFSIGNALED(status))
        fprintf(stderr,"Tracer: Child died from signal %d
", WTERMSIG(status));
    else
        fprintf(stderr,"Tracer: Child vanished
");
    fflush(stderr);

    return status;
}

tracer.c执行指定的命令,等待命令接收到SIGSTOP信号。 (tracer.c本身不会发送;您可以让跟踪停止自身,也可以从外部发送信号。)

好。

命令停止后,tracer.c将ptrace附加到每个线程,并以固定步数(SINGLESTEPS编译时常数)单步执行其中一个线程,显示每个线程的相关寄存器状态。

好。

之后,它将与命令分离,并向其发送SIGCONT信号以使其继续正常运行。

好。

这是我用于测试的简单测试程序worker.c:

好。

1
2
3
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

#ifndef   THREADS
#define   THREADS  2
#endif

volatile sig_atomic_t   done = 0;

void catch_done(int signum)
{
    done = signum;
}

int install_done(const int signum)
{
    struct sigaction act;

    sigemptyset(&act.sa_mask);
    act.sa_handler = catch_done;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL))
        return errno;
    else
        return 0;
}

void *worker(void *data)
{
    volatile unsigned long *const counter = data;

    while (!done)
        __sync_add_and_fetch(counter, 1UL);

    return (void *)(unsigned long)__sync_or_and_fetch(counter, 0UL);
}

int main(void)
{
    unsigned long   counter = 0UL;
    pthread_t       thread[THREADS];
    pthread_attr_t  attrs;
    size_t          i;

    if (install_done(SIGHUP) ||
        install_done(SIGTERM) ||
        install_done(SIGUSR1)) {
        fprintf(stderr,"Worker: Cannot install signal handlers: %s.
", strerror(errno));
        return 1;
    }

    pthread_attr_init(&attrs);
    pthread_attr_setstacksize(&attrs, 65536);
    for (i = 0; i < THREADS; i++)
        if (pthread_create(&thread[i], &attrs, worker, &counter)) {
            done = 1;
            fprintf(stderr,"Worker: Cannot create thread: %s.
", strerror(errno));
            return 1;
        }
    pthread_attr_destroy(&attrs);

    /* Let the original thread also do the worker dance. */
    worker(&counter);

    for (i = 0; i < THREADS; i++)
        pthread_join(thread[i], NULL);

    return 0;
}

编译都使用例如

好。

1
2

gcc -W -Wall -O3 -fomit-frame-pointer worker.c -pthread -o worker
gcc -W -Wall -O3 -fomit-frame-pointer tracer.c -o tracer

并在单独的终端或后台运行,例如

好。

1

./tracer ./worker &

跟踪器显示工作者的PID:

好。

1

Tracer: Waiting for child (pid 24275) events.

此时,孩子正在正常运行。当您向孩子发送SIGSTOP时,动作开始。跟踪器检测到它,进行所需的跟踪,然后分离并让孩子正常继续:

好。

1
2
3
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

kill -STOP 24275

Process 24275 has 3 tasks, attached to all.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a63, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a65, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a58, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a63, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a65, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a58, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8. Advanced by one step.

Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a63, RSP=0x00007f399cfa6ee8. Advanced by one step.

Detached. Waiting for new stop events.

您可以根据需要多次重复上述操作。请注意,我选择了SIGSTOP信号作为触发器,因为tracer.c这样也可以用作根据每个请求生成复杂的多线程核心转储的基础(因为多线程进程可以通过向自身发送SIGSTOP来简单地触发它) 。

好。

在上面的示例中,worker()函数的反汇编使线程全部旋转:

好。

1
2
3
4
5
6
7
8
9
10
11
12

0x400a50: eb 0b                 jmp          0x400a5d
0x400a52: 66 0f 1f 44 00 00     nopw         0x0(%rax,%rax,1)
0x400a58: f0 48 83 07 01        lock addq    $0x1,(%rdi)          = fourth step
0x400a5d: 8b 05 00 00 00 00     mov          0x0(%rip),%eax       = first step
0x400a63: 85 c0                 test         %eax,%eax            = second step
0x400a65: 74 f1                 je           0x400a58             = third step
0x400a67: 48 8b 07              mov          (%rdi),%rax
0x400a6a: 48 89 c2              mov          %rax,%rdx
0x400a6d: f0 48 0f b1 07        lock cmpxchg %rax,(%rdi)
0x400a72: 75 f6                 jne          0x400a6a
0x400a74: 48 89 d0              mov          %rdx,%rax
0x400a77: c3                    retq

现在,该测试程序仅显示了如何停止进程,将其附加到其所有线程,单步执行所需数量的指令,然后让所有线程正常继续;它还不能证明同样适用于让特定线程正常继续运行(通过PTRACE_CONT)。但是,我在下面描述的细节向我表明,对于PTRACE_CONT,相同的方法应该可以正常工作。

好。

我在编写上述测试程序时遇到的主要问题或惊奇是:

好。

1
2
3
4
5

long r;

do {
    r = ptrace(PTRACE_cmd, tid, ...);
} while (r == -1L && (errno == EBUSY || errno == EFAULT || errno == ESRCH));

循环,特别是对于ESRCH情况(由于ptrace手册页描述而仅添加了其他情况)。

好。

您会看到,大多数ptrace命令仅在任务停止时才被允许。但是,任务仍在完成时(例如,单步命令。因此,使用上述循环-可能添加一毫秒的nanosleep或类似操作以避免浪费CPU-确保在尝试提供新的ptrace命令之前,该命令已经完成(因此任务已停止)。

好。

Kerrek SB,我相信至少您在测试程序中遇到的一些麻烦是由于此问题引起的吗?对我个人而言,这是一种D'oh!意识到这一点当然是必要的,因为追踪本质上是异步的,而不是同步的。

好。

(这种异步性也是我上面提到的SIGCONT-PTRACE_CONT交互的原因。我确实相信,使用上述循环正确处理后,交互不再是问题,并且实际上是可以理解的。)

好。

在此答案的注释中添加:

好。

Linux内核在task_struct结构中使用一组任务状态标志(有关定义,请参见include/linux/sched.h)来跟踪每个任务的状态。 ptrace()的面向用户空间的一面在kernel/ptrace.c中定义。

好。

调用PTRACE_SINGLESTEP或PTRACE_CONT时,kernel/ptrace.c:ptrace_continue()处理大多数细节。通过调用wake_up_state(child, __TASK_TRACED)(kernel/sched/core.c::try_to_wake_up(child, __TASK_TRACED, 0))完成。

好。

通过SIGSTOP信号停止进程时,所有任务将停止,并最终处于"已停止,未跟踪"状态。

好。

附加到每个任务(通过PTRACE_ATTACH或PTRACE_SEIZE,请参见kernel/ptrace.c:ptrace_attach())可以修改任务状态。但是,ptrace状态位(请参见include/linux/ptrace.h:PT_常量)与任务可运行状态位(请参见include/linux/sched.h:TASK_常量)是分开的。

好。

附加到任务并向进程发送SIGCONT信号后,停止状态不会立即被修改(我相信),因为也正在跟踪任务。执行PTRACE_SINGLESTEP或PTRACE_CONT的结果以kernel/sched/core.c::try_to_wake_up(child, __TASK_TRACED, 0)结尾,这将更新任务状态,并将任务移至运行队列。

好。

现在,我尚未找到代码路径的复杂部分是下次计划任务时如何在内核中更新任务状态。我的测试表明,通过单步执行(这是另一个任务状态标志),只有任务状态被更新,并且清除了单步标志。看来PTRACE_CONT并不那么可靠。我相信这是因为单步标记"强制"了任务状态的改变。也许有一个"竞赛条件"。继续传递信号和改变状态?

好。

(进一步编辑:内核开发人员肯定希望调用wait(),例如,请参见此线程。)

好。

换句话说,在注意到该进程已停止之后(请注意,如果该进程不是子进程并且尚未附加,则可以使用/proc/PID/stat或/proc/PID/status),我相信以下过程是最可靠的过程:

好。

1
2
3
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

pid_t  pid, p; /* Process owning the tasks */
tid_t *tid;    /* Task ID array */
size_t tids;   /* Tasks */
long   result;
int    status;
size_t i;

for (i = 0; i < tids; i++) {
    while (1) {
        result = ptrace(PTRACE_ATTACH, tid[i], (void *)0, (void *)0);
        if (result == -1L && (errno == ESRCH || errno == EBUSY || errno == EFAULT || errno == EIO)) {
            /* To avoid burning up CPU for nothing: */
            sched_yield(); /* or nanosleep(), or usleep() */
            continue;
        }
        break;
    }       
    if (result == -1L) {
        /*
         * Fatal error. First detach from tid[0..i-1], then exit.
        */
    }
}

/* Send SIGCONT to the process. */
if (kill(pid, SIGCONT)) {
    /*
     * Fatal error, see errno. Exit.
    */
}

/* Since we are attached to the process,
 * we can wait() on it. */
while (1) {
    errno = 0;
    status = 0;
    p = waitpid(pid, &status, WCONTINUED);
    if (p == (pid_t)-1) {
        if (errno == EINTR)
            continue;
        else
            break;
    } else
    if (p != pid) {
        errno = ESRCH;
        break;
    } else
    if (WIFCONTINUED(status)) {
        errno = 0;
        break;
    }
}
if (errno) {
    /*
     * Fatal error. First detach from tid[0..tids-1], then exit.
    */
}

/* Single-step each task to update the task states. */
for (i = 0; i < tids; i++) {
    while (1) {
        result = ptrace(PTRACE_SINGLESTEP, tid[i], (void *)0, (void *)0);
        if (result == -1L && errno == ESRCH) {
            /* To avoid burning up CPU for nothing: */
            sched_yield(); /* or nanosleep(), or usleep() */
            continue;
        }
        break;
    }       
    if (result == -1L) {
        /*
         * Fatal error. First detach from tid[0..i-1], then exit.
        */
    }
}

/* Obtain task register structures, to make sure the single-steps
 * have completed and their states have stabilized. */
for (i = 0; i < tids; i++) {
    struct user_regs_struct regs;

    while (1) {
        result = ptrace(PTRACE_GETREGS, tid[i], &regs, &regs);
        if (result == -1L && (errno == ESRCH || errno == EBUSY || errno == EFAULT || errno == EIO)) {
            /* To avoid burning up CPU for nothing: */
            sched_yield(); /* or nanosleep(), or usleep() */
            continue;
        }
        break;
    }       
    if (result == -1L) {
        /*
         * Fatal error. First detach from tid[0..i-1], then exit.
        */
    }
}

完成上述操作后,所有任务都应附加并处于预期状态,以便例如PTRACE_CONT无需任何技巧就可以工作。

好。

如果行为在将来的内核中发生变化-我确实相信STOP / CONT信号与跟踪之间的相互作用可能会发生变化; 至少应该向LKML开发人员提出有关此行为的问题! -,以上步骤仍然可以正常进行。 (谨慎起见,通过多次使用PTRACE_SINGLESTEP循环可能也是个好主意。)

好。

与PTRACE_CONT的区别在于,如果将来行为发生变化,则初始PTRACE_CONT可能实际上会继续执行该过程,从而导致其后的ptrace()失败。 使用PTRACE_SINGLESTEP,该过程将停止,从而允许进一步的ptrace()调用成功。

好。

有什么问题吗

好。

好。

 相关讨论


Can I attach to a specific thread?

是的,至少在当前内核上是如此。

Does that mean that single-stepping only steps through that one thread's instructions? Does it stop all the process's threads?

是。它不会停止其他线程,只会停止附加的线程。

Is there a way to step forward only in one single thread but guarantee that the other threads remain stopped?

是。将SIGSTOP发送到进程(使用waitpid(PID,,WUNTRACED)等待进程停止),然后将PTRACE_ATTACH发送到进程中的每个线程。发送SIGCONT(使用waitpid(PID,,WCONTINUED)等待该过程继续)。

由于在连接时所有线程均已停止,并且连接停止了线程,因此在传递SIGCONT信号后,所有线程均保持停止状态。您可以按照自己喜欢的任何顺序单步执行线程。

我发现这很有趣,足以激发一个测试案例。 (好吧,实际上我怀疑无论如何都不会相信我的话,所以我决定最好证明自己可以复制自己。)

我的系统似乎遵循Linux手册页项目中所述的man 2 ptrace,而Kerrisk似乎非常擅长保持它们与内核行为同步。总的来说,我更喜欢kernel.org的wrt。 Linux内核的其他来源。

摘要:

  • 附加到进程本身(TID == PID)只会停止原始线程,而不会停止所有线程。

  • 附加到特定线程(使用/proc/PID/task/中的TID)不会停止该线程。 (换句话说,TID == PID的线程并不特殊。)

  • 发送SIGSTOP到进程将停止所有线程,但是ptrace()仍然可以正常工作。

  • 如果您向该进程发送了SIGSTOP,请在分离前不要调用ptrace(PTRACE_CONT, TID)。 PTRACE_CONT似乎会干扰SIGCONT信号。

    您可以先发送SIGSTOP,然后发送PTRACE_ATTACH,然后发送SIGCONT,没有任何问题。线程将保持停止状态(由于ptrace)。换句话说,PTRACE_ATTACH和PTRACE_DETACH与SIGSTOP和SIGCONT混合得很好,没有任何副作用。

  • 即使您尝试使用tgkill()(或pthread_kill())将信号发送到特定线程,SIGSTOP和SIGCONT也会影响整个过程。

  • 要停止并继续某个特定线程,请PTHREAD_ATTACH;要停止和继续某个进程的所有线程,分别向该进程发送SIGSTOP和SIGCONT信号。

就我个人而言,我相信这可以验证我在另一个问题中建议的方法。

这是您可以编译运行的难看的测试代码,traces.c:

1
2
3
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365

#define  GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <dirent.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>

#ifndef   THREADS
#define   THREADS  3
#endif

static int tgkill(int tgid, int tid, int sig)
{
    int retval;

    retval = syscall(SYS_tgkill, tgid, tid, sig);
    if (retval < 0) {
        errno = -retval;
        return -1;
    }

    return 0;
}

volatile unsigned long counter[THREADS + 1] = { 0UL };

volatile sig_atomic_t run = 0;
volatile sig_atomic_t done = 0;

void handle_done(int signum)
{
    done = signum;
}

int install_done(int signum)
{
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_done;
    act.sa_flags = 0;
    if (sigaction(signum, &act, NULL))
        return errno;
    return 0;
}

void *worker(void *data)
{
    volatile unsigned long *const counter = data;

    while (!run)
        ;

    while (!done)
        (*counter)++;

    return (void *)(*counter);
}

pid_t *gettids(const pid_t pid, size_t *const countptr)
{
    char           dirbuf[128];
    DIR           *dir;
    struct dirent *ent;

    pid_t         *data = NULL, *temp;
    size_t         size = 0;
    size_t         used = 0;

    int            tid;
    char           dummy;

    if ((int)pid < 2) {
        errno = EINVAL;
        return NULL;
    }

    if (snprintf(dirbuf, sizeof dirbuf,"/proc/%d/task/", (int)pid) >= (int)sizeof dirbuf) {
        errno = ENAMETOOLONG;
        return NULL;
    }

    dir = opendir(dirbuf);
    if (!dir)
        return NULL;

    while (1) {
        errno = 0;
        ent = readdir(dir);
        if (!ent)
            break;

        if (sscanf(ent->d_name,"%d%c", &tid, &dummy) != 1)
            continue;

        if (tid < 2)
            continue;

        if (used >= size) {
            size = (used | 127) + 129;
            temp = realloc(data, size * sizeof data[0]);
            if (!temp) {
                free(data);
                closedir(dir);
                errno = ENOMEM;
                return NULL;
            }
            data = temp;
        }

        data[used++] = (pid_t)tid;
    }
    if (errno) {
        free(data);
        closedir(dir);
        errno = EIO;
        return NULL;
    }
    if (closedir(dir)) {
        free(data);
        errno = EIO;
        return NULL;
    }

    if (used < 1) {
        free(data);
        errno = ENOENT;
        return NULL;
    }

    size = used + 1;
    temp = realloc(data, size * sizeof data[0]);
    if (!temp) {
        free(data);
        errno = ENOMEM;
        return NULL;
    }
    data = temp;

    data[used] = (pid_t)0;

    if (countptr)
        *countptr = used;

    errno = 0;
    return data;
}

int child_main(void)
{
    pthread_t   id[THREADS];
    int         i;

    if (install_done(SIGUSR1)) {
        fprintf(stderr,"Cannot set SIGUSR1 signal handler.
");
        return 1;
    }

    for (i = 0; i < THREADS; i++)
        if (pthread_create(&id[i], NULL, worker, (void *)&counter[i])) {
            fprintf(stderr,"Cannot create thread %d of %d: %s.
", i + 1, THREADS, strerror(errno));
            return 1;
        }

    run = 1;

    kill(getppid(), SIGUSR1);

    while (!done)
        counter[THREADS]++;

    for (i = 0; i < THREADS; i++)
        pthread_join(id[i], NULL);

    printf("Final counters:
");
    for (i = 0; i < THREADS; i++)
        printf("\tThread %d: %lu
", i + 1, counter[i]);
    printf("\tMain thread: %lu
", counter[THREADS]);

    return 0;
}

int main(void)
{
    pid_t   *tid = NULL;
    size_t   tids = 0;
    int      i, k;
    pid_t    child, p;

    if (install_done(SIGUSR1)) {
        fprintf(stderr,"Cannot set SIGUSR1 signal handler.
");
        return 1;
    }

    child = fork();
    if (!child)
        return child_main();

    if (child == (pid_t)-1) {
        fprintf(stderr,"Cannot fork.
");
        return 1;
    }

    while (!done)
        usleep(1000);

    tid = gettids(child, &tids);
    if (!tid) {
        fprintf(stderr,"gettids(): %s.
", strerror(errno));
        kill(child, SIGUSR1);
        return 1;
    }

    fprintf(stderr,"Child process %d has %d tasks.
", (int)child, (int)tids);
    fflush(stderr);

    for (k = 0; k < (int)tids; k++) {
        const pid_t t = tid[k];

        if (ptrace(PTRACE_ATTACH, t, (void *)0L, (void *)0L)) {
            fprintf(stderr,"Cannot attach to TID %d: %s.
", (int)t, strerror(errno));
            kill(child, SIGUSR1);
            return 1;
        }

        fprintf(stderr,"Attached to TID %d.

", (int)t);

        fprintf(stderr,"Peeking the counters in the child process:
");
        for (i = 0; i <= THREADS; i++) {
            long v;
            do {
                errno = 0;
                v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
            } while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
            fprintf(stderr,"\tcounter[%d] = %lu
", i, (unsigned long)v);
        }
        fprintf(stderr,"Waiting a short moment ...");
        fflush(stderr);

        usleep(250000);

        fprintf(stderr,"and another peek:
");
        for (i = 0; i <= THREADS; i++) {
            long v;
            do {
                errno = 0;
                v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
            } while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
            fprintf(stderr,"\tcounter[%d] = %lu
", i, (unsigned long)v);
        }
        fprintf(stderr,"
");
        fflush(stderr);

        usleep(250000);

        ptrace(PTRACE_DETACH, t, (void *)0L, (void *)0L);
    }

    for (k = 0; k < 4; k++) {
        const pid_t t = tid[tids / 2];

        if (k == 0) {
            fprintf(stderr,"Sending SIGSTOP to child process ...");
            fflush(stderr);
            kill(child, SIGSTOP);
        } else
        if (k == 1) {
            fprintf(stderr,"Sending SIGCONT to child process ...");
            fflush(stderr);
            kill(child, SIGCONT);
        } else
        if (k == 2) {
            fprintf(stderr,"Sending SIGSTOP to TID %d ...", (int)tid[0]);
            fflush(stderr);
            tgkill(child, tid[0], SIGSTOP);
        } else
        if (k == 3) {
            fprintf(stderr,"Sending SIGCONT to TID %d ...", (int)tid[0]);
            fflush(stderr);
            tgkill(child, tid[0], SIGCONT);
        }
        usleep(250000);
        fprintf(stderr,"done.
");
        fflush(stderr);

        if (ptrace(PTRACE_ATTACH, t, (void *)0L, (void *)0L)) {
            fprintf(stderr,"Cannot attach to TID %d: %s.
", (int)t, strerror(errno));
            kill(child, SIGUSR1);
            return 1;
        }

        fprintf(stderr,"Attached to TID %d.

", (int)t);

        fprintf(stderr,"Peeking the counters in the child process:
");
        for (i = 0; i <= THREADS; i++) {
            long v;
            do {
                errno = 0;
                v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
            } while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
            fprintf(stderr,"\tcounter[%d] = %lu
", i, (unsigned long)v);
        }
        fprintf(stderr,"Waiting a short moment ...");
        fflush(stderr);

        usleep(250000);

        fprintf(stderr,"and another peek:
");
        for (i = 0; i <= THREADS; i++) {
            long v;
            do {
                errno = 0;
                v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
            } while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
            fprintf(stderr,"\tcounter[%d] = %lu
", i, (unsigned long)v);
        }
        fprintf(stderr,"
");
        fflush(stderr);

        usleep(250000);

        ptrace(PTRACE_DETACH, t, (void *)0L, (void *)0L);
    }

    kill(child, SIGUSR1);

    do {
        p = waitpid(child, NULL, 0);
        if (p == -1 && errno != EINTR)
            break;
    } while (p != child);

    return 0;
}

使用例如编译并运行

1
2

gcc -DTHREADS=3 -W -Wall -O3 traces.c -pthread -o traces
./traces

输出是子进程计数器的转储(每个计数器在单独的线程中递增,包括使用最终计数器的原始线程)。在短暂的等待时间内比较计数器。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13

Child process 18514 has 4 tasks.
Attached to TID 18514.

Peeking the counters in the child process:
    counter[0] = 0
    counter[1] = 0
    counter[2] = 0
    counter[3] = 0
Waiting a short moment ... and another peek:
    counter[0] = 18771865
    counter[1] = 6435067
    counter[2] = 54247679
    counter[3] = 0

如上所示,只有使用最终计数器的初始线程(其TID == PID)停止了。其他三个线程也是如此,它们按顺序使用前三个计数器:

1
2
3
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
33
34
35
36
37
38

Attached to TID 18515.

Peeking the counters in the child process:
    counter[0] = 25385151
    counter[1] = 13459822
    counter[2] = 103763861
    counter[3] = 560872
Waiting a short moment ... and another peek:
    counter[0] = 25385151
    counter[1] = 69116275
    counter[2] = 120500164
    counter[3] = 9027691

Attached to TID 18516.

Peeking the counters in the child process:
    counter[0] = 25397582
    counter[1] = 105905400
    counter[2] = 155895025
    counter[3] = 17306682
Waiting a short moment ... and another peek:
    counter[0] = 32358651
    counter[1] = 105905400
    counter[2] = 199601078
    counter[3] = 25023231

Attached to TID 18517.

Peeking the counters in the child process:
    counter[0] = 40600813
    counter[1] = 111675002
    counter[2] = 235428637
    counter[3] = 32298929
Waiting a short moment ... and another peek:
    counter[0] = 48727731
    counter[1] = 143870702
    counter[2] = 235428637
    counter[3] = 39966259

接下来的两种情况检查SIGCONT / SIGSTOP wrt。整个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Sending SIGSTOP to child process ... done.
Attached to TID 18516.

Peeking the counters in the child process:
    counter[0] = 56887263
    counter[1] = 170646440
    counter[2] = 235452621
    counter[3] = 48077803
Waiting a short moment ... and another peek:
    counter[0] = 56887263
    counter[1] = 170646440
    counter[2] = 235452621
counter[3] = 48077803

Sending SIGCONT to child process ... done.
Attached to TID 18516.

Peeking the counters in the child process:
    counter[0] = 64536344
    counter[1] = 182359343
    counter[2] = 253660731
    counter[3] = 56422231
Waiting a short moment ... and another peek:
    counter[0] = 72029244
    counter[1] = 182359343
    counter[2] = 288014365
    counter[3] = 63797618

如您所见,发送SIGSTOP将停止所有线程,但不会妨碍ptrace()。同样,在SIGCONT之后,线程继续正常运行。

最后两个案例检查了使用tgkill()将SIGSTOP / SIGCONT发送到特定线程(与第一个计数器对应的线程)并附加到另一个线程时的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Sending SIGSTOP to TID 18514 ... done.
Attached to TID 18516.

Peeking the counters in the child process:
    counter[0] = 77012930
    counter[1] = 183059526
    counter[2] = 344043770
    counter[3] = 71120227
Waiting a short moment ... and another peek:
    counter[0] = 77012930
    counter[1] = 183059526
    counter[2] = 344043770
    counter[3] = 71120227

Sending SIGCONT to TID 18514 ... done.
Attached to TID 18516.

Peeking the counters in the child process:
    counter[0] = 88082419
    counter[1] = 194059048
    counter[2] = 359342314
    counter[3] = 84887463
Waiting a short moment ... and another peek:
    counter[0] = 100420161
    counter[1] = 194059048
    counter[2] = 392540525
    counter[3] = 111770366

不幸的是,但是正如预期的那样,处置(停止/运行)是整个过程的,而不是特定于线程的,如您在上面看到的。这意味着要停止特定线程并让其他线程正常运行,您需要分别对要停止的线程PTHREAD_ATTACH。

为了证明我上面的所有陈述,您可能必须添加测试用例;我最终得到了相当多的代码副本,所有代码都进行了稍微的编辑,以测试全部内容,而且我不确定我是否选择了最完整的代码集。如果您发现有遗漏,我很乐意扩展测试程序。

有什么问题吗

 相关讨论


进程中的每个线程都被单独跟踪(并且每个线程都可能通过不同的跟踪过程进行跟踪,或者被跟踪)。当您调用ptrace attach时,您总是只附加到一个线程。只有该线程将停止-其他线程将继续按原样运行。

ptrace()手册页的最新版本非常清楚:

Attachment and subsequent commands are per thread: in a multithreaded
process, every thread can be individually attached to a (potentially
different) tracer, or left not attached and thus not debugged.
Therefore,"tracee" always means"(one) thread", never"a (possibly
multithreaded) process". Ptrace commands are always sent to a
specific tracee using a call of the form

1

ptrace(PTRACE_foo, pid, ...)

where pid is the thread ID of the corresponding Linux thread.

(Note that in this page, a"multithreaded process" means a thread
group consisting of threads created using the clone(2)
CLONE_THREAD flag.)

单步执行仅影响您将其定向到的线程。如果其他线程正在运行,则它们将继续运行,如果它们在跟踪停止中,则它们将保持在跟踪停止状态。 (这意味着,如果您正在单步执行的线程尝试获取互斥量或另一个未运行线程所拥有的类似同步资源,则它将无法获取该互斥量)。

如果要在单步执行一个线程时停止该进程的所有线程,则将需要附加到所有线程。更为复杂的是,如果在尝试附加到进程时进程正在运行,则在枚举线程时可能会创建新线程。

 相关讨论


Does it stop all the process's threads?


它跟踪该进程,该进程的所有线程都停止。
想象一下,这不是您如何在IDE中看到不同的线程。

从手册:

The ptrace() system call provides a means by which one process (the"tracer") may observe and control the execution of another process (the"tracee")

附带的示例代码:

1
2
3
4
5
6

printf("Attaching to process %d
",Tpid);
if ((ptrace(PTRACE_ATTACH, Tpid, 0, 0)) != 0) {;
    printf("Attach result %d
",res);
}

 

因此,是的,您可以使用一个线程,是的,它将停止该进程的所有线程。

1
2
3
4
5

if ((res = ptrace(PTRACE_SINGLESTEP, Tpid, 0, signo)) < 0) {
perror("Ptrace singlestep error");
exit(1);
}
res = wait(&stat);

原链接:关于C#:如何使用PTRACE获得多个线程的一致视图? | 码农家园 

标签:errno,counter,pid,附加,线程,ptrace,stderr,fprintf,多线程
来源: https://blog.csdn.net/u013043103/article/details/120369007

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有