source: neercs/trunk/src/mytrace.c @ 3326

Last change on this file since 3326 was 3326, checked in by Sam Hocevar, 12 years ago

mytrace.c: disambiguate error messages and use perror() instead of fprintf().

  • Property svn:eol-style set to native
File size: 16.5 KB
RevLine 
[2491]1/*
2 *  neercs        console-based window manager
3 *  Copyright (c) 2008 Pascal Terjan
[2509]4 *            (c) 2008 Sam Hocevar <sam@zoy.org>
[2491]5 *                All Rights Reserved
6 *
7 *  $Id$
8 *
9 *  This program is free software. It comes without any warranty, to
10 *  the extent permitted by applicable law. You can redistribute it
11 *  and/or modify it under the terms of the Do What The Fuck You Want
12 *  To Public License, Version 2, as published by Sam Hocevar. See
13 *  http://sam.zoy.org/wtfpl/COPYING for more details.
14 */
15
16#include "config.h"
17
[2426]18#include <errno.h>
19#include <fcntl.h>
[2999]20#include <limits.h>
[2426]21#include <stdio.h>
22#include <stdlib.h>
23#include <string.h>
24
[2455]25#if defined USE_GRAB
[2612]26#   include <sys/ptrace.h>
[2455]27#   include <sys/stat.h>
28#   include <sys/syscall.h>
29#   include <sys/user.h>
30#   include <sys/wait.h>
31#endif
[2450]32
[2426]33#include "neercs.h"
[2509]34#include "mytrace.h"
[2426]35
[2509]36#if defined USE_GRAB
37static int memcpy_from_target(struct mytrace *t,
38                              char* dest, long src, size_t n);
39static int memcpy_into_target(struct mytrace *t,
[2511]40                              long dest, char const *src, size_t n);
[2516]41static long remote_syscall(struct mytrace *t, long call,
[2509]42                           long arg1, long arg2, long arg3);
[2511]43#   if defined DEBUG
[2509]44static void print_registers(pid_t pid);
[2511]45#   else
46#       define print_registers(x) do {} while(0)
47#   endif
[2509]48
[2505]49#define X(x) #x
50#define STRINGIFY(x) X(x)
51
[2785]52#define SYSCALL_X86     0x80cd  /* CD 80 = int $0x80 */
53#define SYSCALL_X86_NEW 0xf3eb  /* EB F3 = jmp <__kernel_vsyscall+0x3> */
54#define SYSENTER        0x340f  /* 0F 34 = sysenter */
55#define SYSCALL_AMD64   0x050fL /* 0F 05 = syscall */
[2511]56
[2501]57#if defined __x86_64__
58#   define RAX rax
59#   define RBX rbx
60#   define RCX rcx
61#   define RDX rdx
62#   define RSP rsp
[2786]63#   define RBP rbp
[2501]64#   define RIP rip
[2505]65#   define RDI rdi
66#   define RSI rsi
67#   define FMT "%016lx"
[2501]68#else
69#   define RAX eax
70#   define RBX ebx
71#   define RCX ecx
72#   define RDX edx
73#   define RSP esp
[2786]74#   define RBP ebp
[2501]75#   define RIP eip
[2505]76#   define RDI edi
77#   define RSI esi
78#   define FMT "%08lx"
[2501]79#endif
80
[2516]81#define MYCALL_OPEN     0
82#define MYCALL_CLOSE    1
83#define MYCALL_WRITE    2
84#define MYCALL_DUP2     3
85#define MYCALL_SETPGID  4
86#define MYCALL_SETSID   5
87#define MYCALL_KILL     6
88#define MYCALL_FORK     7
89#define MYCALL_EXIT     8
[2999]90#define MYCALL_EXECVE   9
[2516]91
[2513]92#if defined __x86_64__
[2516]93/* from unistd_32.h on an amd64 system */
[2999]94int syscalls32[] = { 5, 6, 4, 63, 57, 66, 37, 2, 1, 11 };
[2513]95int syscalls64[] =
96#else
97int syscalls32[] =
98#endif
[2514]99    { SYS_open, SYS_close, SYS_write, SYS_dup2, SYS_setpgid, SYS_setsid,
[2999]100      SYS_kill, SYS_fork, SYS_exit, SYS_execve };
[2513]101
[2516]102char const *syscallnames[] =
103    { "open", "close", "write", "dup2", "setpgid", "setsid", "kill", "fork",
[2999]104      "exit", "execve" };
[2516]105
[3044]106#endif /* USE_GRAB */
107
[2509]108struct mytrace
109{
[2516]110    pid_t pid, child;
[2509]111};
112
113struct mytrace* mytrace_attach(long int pid)
114{
[2455]115#if defined USE_GRAB
[2509]116    struct mytrace *t;
[2516]117    int status;
[2509]118
119    if(ptrace(PTRACE_ATTACH, pid, 0, 0) < 0)
120    {
[3326]121        perror("PTRACE_ATTACH (attach)");
[2509]122        return NULL;
123    }
[2516]124    if(waitpid(pid, &status, 0) < 0)
125    {
126        perror("waitpid");
127        return NULL;
128    }
129    if(!WIFSTOPPED(status))
130    {
131        fprintf(stderr, "traced process was not stopped\n");
132        ptrace(PTRACE_DETACH, pid, 0, 0);
133        return NULL;
134    }
[2509]135
136    t = malloc(sizeof(struct mytrace));
137    t->pid = pid;
[2516]138    t->child = 0;
[2509]139
140    return t;
141#else
142    errno = ENOSYS;
143    return NULL;
144#endif
145}
146
[2516]147struct mytrace* mytrace_fork(struct mytrace *t)
148{
149#if defined USE_GRAB
150    struct mytrace *child;
151
152    ptrace(PTRACE_SETOPTIONS, t->pid, NULL, PTRACE_O_TRACEFORK);
153    remote_syscall(t, MYCALL_FORK, 0, 0, 0);
154    waitpid(t->child, NULL, 0);
155
156    child = malloc(sizeof(struct mytrace));
157    child->pid = t->child;
158    child->child = 0;
159
160    return child;
161#else
162    errno = ENOSYS;
163    return NULL;
164#endif
165}
166
[2509]167int mytrace_detach(struct mytrace *t)
[2426]168{
[2509]169#if defined USE_GRAB
170    ptrace(PTRACE_DETACH, t->pid, 0, 0);
171    free(t);
172
173    return 0;
174#else
175    errno = ENOSYS;
176    return -1;
177#endif
178}
179
[2516]180long mytrace_getpid(struct mytrace *t)
181{
182#if defined USE_GRAB
183    return t->pid;
184#else
185    errno = ENOSYS;
186    return -1;
187#endif
188}
189
[2513]190int mytrace_open(struct mytrace *t, char const *path, int mode)
[2509]191{
192#if defined USE_GRAB
193    char backup_data[4096];
[2505]194    struct user_regs_struct regs;
[2509]195    size_t size = strlen(path) + 1;
196    int ret;
[2505]197
[2509]198    if(ptrace(PTRACE_GETREGS, t->pid, NULL, &regs) < 0)
[2505]199    {
[3326]200        perror("PTRACE_GETREGS (open)\n");
[2509]201        return errno;
[2505]202    }
203
[2509]204    /* Backup the data that we will use */
205    if(memcpy_from_target(t, backup_data, regs.RSP, size) < 0)
206        return -1;
[2505]207
[2509]208    memcpy_into_target(t, regs.RSP, path, size);
[2505]209
[2516]210    ret = remote_syscall(t, MYCALL_OPEN, regs.RSP, O_RDWR, 0755);
[2509]211
[2511]212    /* Restore the data */
[2509]213    memcpy_into_target(t, regs.RSP, backup_data, size);
214
215    if(ret < 0)
[2505]216    {
[2509]217        errno = ret;
218        return -1;
219    }
220
221    return ret;
[2505]222#else
[2509]223    errno = ENOSYS;
224    return -1;
[2505]225#endif
226}
227
[2509]228int mytrace_close(struct mytrace *t, int fd)
[2505]229{
[2509]230#if defined USE_GRAB
[2516]231    return remote_syscall(t, MYCALL_CLOSE, fd, 0, 0);
[2509]232#else
233    errno = ENOSYS;
234    return -1;
235#endif
236}
237
[2513]238int mytrace_write(struct mytrace *t, int fd, char const *data, size_t len)
239{
240#if defined USE_GRAB
241    struct user_regs_struct regs;
242    char *backup_data;
243    int ret;
244
245    if(ptrace(PTRACE_GETREGS, t->pid, NULL, &regs) < 0)
246    {
[3326]247        perror("PTRACE_GETREGS (write)\n");
[2513]248        return errno;
249    }
250
251    backup_data = malloc(len);
252
253    /* Backup the data that we will use */
254    if(memcpy_from_target(t, backup_data, regs.RSP, len) < 0)
255        return -1;
256
257    memcpy_into_target(t, regs.RSP, data, len);
258
[2516]259    ret = remote_syscall(t, MYCALL_WRITE, fd, regs.RSP, len);
[2513]260
261    /* Restore the data */
262    memcpy_into_target(t, regs.RSP, backup_data, len);
263
264    if(ret < 0)
265    {
266        errno = ret;
267        return -1;
268    }
269
270    return ret;
271#else
272    errno = ENOSYS;
273    return -1;
274#endif
275}
276
[2509]277int mytrace_dup2(struct mytrace *t, int oldfd, int newfd)
278{
279#if defined USE_GRAB
[2516]280    return remote_syscall(t, MYCALL_DUP2, oldfd, newfd, 0);
[2509]281#else
282    errno = ENOSYS;
283    return -1;
284#endif
285}
286
287int mytrace_setpgid(struct mytrace *t, long pid, long pgid)
288{
289#if defined USE_GRAB
[2516]290    return remote_syscall(t, MYCALL_SETPGID, pid, pgid, 0);
[2509]291#else
292    errno = ENOSYS;
293    return -1;
294#endif
295}
296
297int mytrace_setsid(struct mytrace *t)
298{
299#if defined USE_GRAB
[2516]300    return remote_syscall(t, MYCALL_SETSID, 0, 0, 0);
[2509]301#else
302    errno = ENOSYS;
303    return -1;
304#endif
305}
306
[2514]307int mytrace_kill(struct mytrace *t, long pid, int sig)
308{
309#if defined USE_GRAB
[2516]310    return remote_syscall(t, MYCALL_KILL, pid, sig, 0);
[2514]311#else
312    errno = ENOSYS;
313    return -1;
314#endif
315}
316
[2516]317int mytrace_exit(struct mytrace *t, int status)
318{
319#if defined USE_GRAB
320    ptrace(PTRACE_SETOPTIONS, t->pid, NULL, PTRACE_O_TRACEEXIT);
321    return remote_syscall(t, MYCALL_EXIT, status, 0, 0);
322#else
323    errno = ENOSYS;
324    return -1;
325#endif
326}
327
[2999]328int mytrace_exec(struct mytrace *t, char const *command)
[2906]329{
330#if defined USE_GRAB
331    struct user_regs_struct regs;
[3320]332    char *env, *p;
333    long p2, envaddr, argvaddr, envptraddr;
[2999]334    char envpath[PATH_MAX+1];
[3320]335    ssize_t envsize = 16*1024;
336    int ret, fd, l, l2;
[3322]337    char *nullp = NULL;
[2999]338    ssize_t r;
[2906]339
[3320]340    ptrace(PTRACE_SETOPTIONS, t->pid, NULL, PTRACE_O_TRACEEXEC);
341
[2906]342    if(ptrace(PTRACE_GETREGS, t->pid, NULL, &regs) < 0)
343    {
[3326]344        perror("PTRACE_GETREGS (exec)\n");
[2906]345        return errno;
346    }
347
[3320]348    debug("PTRACE_GETREGS done");
[2999]349    env = malloc(envsize);
350    if (!env)
[2906]351        return -1;
352
[2999]353    snprintf(envpath, PATH_MAX, "/proc/%d/environ", t->pid);
[2906]354
[2999]355    fd = open(envpath, O_RDONLY);
[3000]356    if (fd == -1)
357        return -1;
[2999]358    r = read(fd, env, envsize);
359    close(fd);
360    if (r == -1)
[2906]361        return -1;
[2999]362    while (r == envsize)
[2906]363    {
[2999]364        free(env);
365        env = malloc(envsize);
366        if (!env)
367            return -1;
368        fd = open(envpath, O_RDONLY);
369        r = read(fd, env, envsize);
370        close(fd);
371        if (r == -1)
372            return -1;
[2906]373    }
[3320]374    envsize = r;
[3322]375    l2 = sizeof(char *); /* Size of a pointer */
376    p2 = regs.RSP;
377
378    /* First argument is the command string */
[2999]379    l = strlen(command)+1;
[3320]380    memcpy_into_target(t, p2, command, l);
381    p2 += l;
[3322]382
383    /* Second argument is argv */
[3320]384    argvaddr = p2;
[3322]385    /* argv[0] is a pointer to the command string */
[3320]386    memcpy_into_target(t, p2, (char *)&regs.RSP, l2);
387    p2 += l2;
[3322]388    /* Then follows a NULL pointer */
389    memcpy_into_target(t, p2, (char *)&nullp, l2);
[3320]390    p2 += l2;
[3322]391
392    /* Third argument is the environment */
393    /* First, copy all the strings */
[3320]394    memcpy_into_target(t, p2, env, envsize);
395    envaddr = p2;
396    p2 += envsize;
[3322]397    /* Then write an array of pointers to the strings */
[3320]398    envptraddr = p2;
399    p = env;
400    while (p < env+envsize)
401    {
402        long diffp = p - env + envaddr;
403        memcpy_into_target(t, p2, (char *)&diffp, l2);
404        p2 += l2;
405        p += strlen(p)+1;
406    }
[3322]407    /* And have a NULL pointer at the end of the array */
408    memcpy_into_target(t, p2, (char *)&nullp, l2);
[2999]409    free(env);
[3322]410
[3320]411    ret = remote_syscall(t, MYCALL_EXECVE, regs.RSP, argvaddr, envptraddr);
[2906]412
413    if(ret < 0)
414    {
415        errno = ret;
416        return -1;
417    }
418
419    return ret;
420#else
421    errno = ENOSYS;
422    return -1;
423#endif
424}
425
[2509]426/*
427 * XXX: the following functions are local
428 */
429
[2511]430#if defined USE_GRAB
[2509]431static int memcpy_from_target(struct mytrace *t,
432                              char* dest, long src, size_t n)
433{
[2511]434    static int const align = sizeof(long) - 1;
435
[2508]436    while(n)
437    {
438        long data;
439        size_t todo = sizeof(long) - (src & align);
[2505]440
[2508]441        if(n < todo)
442            todo = n;
443
[2509]444        data = ptrace(PTRACE_PEEKTEXT, t->pid, src - (src & align), 0);
[2504]445        if(errno)
[2502]446        {
[3326]447            perror("ptrace_peektext (memcpy_from_target)");
[2504]448            return -1;
449        }
[2508]450        memcpy(dest, (char *)&data + (src & align), todo);
451
452        dest += todo;
453        src += todo;
454        n -= todo;
[2426]455    }
[2508]456
[2426]457    return 0;
458}
459
[2509]460static int memcpy_into_target(struct mytrace *t,
[2511]461                              long dest, char const *src, size_t n)
[2426]462{
[2511]463    static int const align = sizeof(long) - 1;
464
[2508]465    while(n)
466    {
467        long data;
468        size_t todo = sizeof(long) - (dest & align);
[2505]469
[2508]470        if(n < todo)
471            todo = n;
472        if(todo != sizeof(long))
[2502]473        {
[2509]474            data = ptrace(PTRACE_PEEKTEXT, t->pid, dest - (dest & align), 0);
[2508]475            if(errno)
476            {
[3326]477                perror("ptrace_peektext (memcpy_into_target)");
[2508]478                return -1;
479            }
480        }
481
482        memcpy((char *)&data + (dest & align), src, todo);
[2509]483        ptrace(PTRACE_POKETEXT, t->pid, dest - (dest & align), data);
[2508]484        if(errno)
485        {
[3326]486            perror("ptrace_poketext (memcpy_into_target)");
[2504]487            return -1;
488        }
[2508]489
490        src += todo;
491        dest += todo;
492        n -= todo;
[2426]493    }
[2508]494
[2426]495    return 0;
496}
497
[2516]498static long remote_syscall(struct mytrace *t, long call,
499                           long arg1, long arg2, long arg3)
[2426]500{
[2503]501    /* Method for remote syscall:
[2505]502     *  - wait until the traced application exits from a syscall
503     *  - save registers
504     *  - rewind eip/rip to point on the syscall instruction
505     *  - single step: execute syscall instruction
[2511]506     *  - retrieve resulting registers
[2505]507     *  - restore registers */
508    struct user_regs_struct regs, oldregs;
[2426]509    long oinst;
[2511]510    int bits;
[2785]511    int offset = 2;
[2426]512
[2516]513    if(call < 0 || call >= (long)(sizeof(syscallnames)/sizeof(*syscallnames)))
514    {
515        fprintf(stderr, "unknown remote syscall %li\n", call);
516        return -1;
517    }
518
[3320]519    debug("remote syscall %s(0x%lx, 0x%lx, 0x%lx)",
[2516]520          syscallnames[call], arg1, arg2, arg3);
521
[2511]522#if defined __x86_64__
523    bits = 64;
524#else
525    bits = 32;
526#endif
527
[2505]528    for(;;)
[2426]529    {
[2516]530        if(ptrace(PTRACE_GETREGS, t->pid, NULL, &oldregs) < 0)
[2505]531        {
[3326]532            perror("PTRACE_GETREGS (syscall 1)\n");
[2505]533            return -1;
534        }
[2426]535
[2516]536        oinst = ptrace(PTRACE_PEEKTEXT, t->pid, oldregs.RIP - 2, 0) & 0xffff;
[2426]537
[2511]538#if defined __x86_64__
539        if(oinst == SYSCALL_AMD64)
[2505]540            break;
[3325]541#endif
[2785]542        if(oinst == SYSCALL_X86 || oinst == SYSCALL_X86_NEW)
[2511]543        {
544            bits = 32;
545            break;
546        }
[2426]547
[2516]548        if(ptrace(PTRACE_SYSCALL, t->pid, NULL, 0) < 0)
[2505]549        {
550            perror("ptrace_syscall (1)");
551            return -1;
552        }
[2516]553        waitpid(t->pid, NULL, 0);
554        if(ptrace(PTRACE_SYSCALL, t->pid, NULL, 0) < 0)
[2505]555        {
556            perror("ptrace_syscall (2)");
557            return -1;
558        }
[2516]559        waitpid(t->pid, NULL, 0);
[2426]560    }
561
[2516]562    print_registers(t->pid);
[2511]563
[2785]564    if(oinst == SYSCALL_X86_NEW)
565    {
566        /*  Get back to sysenter */
567        while((ptrace(PTRACE_PEEKTEXT, t->pid, oldregs.RIP - offset, 0) & 0xffff) != 0x340f)
568            offset++;
[2786]569        oldregs.RBP = oldregs.RSP;
[2785]570    }
571
[2505]572    regs = oldregs;
[2785]573    regs.RIP = regs.RIP - offset;
[2505]574#if defined __x86_64__
[2511]575    if(bits == 64)
576    {
577        regs.RAX = syscalls64[call];
578        regs.RDI = arg1;
579        regs.RSI = arg2;
580        regs.RDX = arg3;
581    }
582    else
[2505]583#endif
[2511]584    {
585        regs.RAX = syscalls32[call];
586        regs.RBX = arg1;
587        regs.RCX = arg2;
588        regs.RDX = arg3;
589    }
[2503]590
[2516]591    if(ptrace(PTRACE_SETREGS, t->pid, NULL, &regs) < 0)
[2426]592    {
[3326]593        perror("PTRACE_SETREGS (syscall 1)\n");
[2505]594        return -1;
[2426]595    }
596
[2516]597    for(;;)
598    {
599        int status;
[2511]600
[2516]601        print_registers(t->pid);
602
603        if(ptrace(PTRACE_SINGLESTEP, t->pid, NULL, NULL) < 0)
604        {
[3326]605            perror("PTRACE_SINGLESTEP (syscall)\n");
[2516]606            return -1;
607        }
608        waitpid(t->pid, &status, 0);
609
610        if(WIFEXITED(status))
611            return 0;
612
613        if(!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP)
614            continue;
615
616        /* Fuck Linux: there is no macro for this */
617        switch((status >> 16) & 0xffff)
618        {
619        case PTRACE_EVENT_FORK:
620            if(ptrace(PTRACE_GETEVENTMSG, t->pid, 0, &t->child) < 0)
621            {
[3326]622                perror("PTRACE_GETEVENTMSG (syscall)\n");
[2516]623                return -1;
624            }
[2785]625            debug("PTRACE_GETEVENTMSG %d", t->child);
[2516]626            continue;
627        case PTRACE_EVENT_EXIT:
[2785]628            debug("PTRACE_EVENT_EXIT");
[2516]629            /* The process is about to exit, don't do anything else */
630            return 0;
[3320]631        case PTRACE_EVENT_EXEC:
632            debug("PTRACE_EVENT_EXEC");
633            return 0;
[2516]634        }
635
636        break;
[2426]637    }
638
[2516]639    print_registers(t->pid);
[2511]640
[2516]641    if(ptrace(PTRACE_GETREGS, t->pid, NULL, &regs) < 0)
[2450]642    {
[3326]643        perror("PTRACE_GETREGS (syscall 2)\n");
[2505]644        return -1;
[2450]645    }
646
[2516]647    if(ptrace(PTRACE_SETREGS, t->pid, NULL, &oldregs) < 0)
[2426]648    {
[3326]649        perror("PTRACE_SETREGS (syscall 2)\n");
[2505]650        return -1;
[2426]651    }
[2516]652    print_registers(t->pid);
[2426]653
[2516]654    debug("syscall %s returned %ld", syscallnames[call], regs.RAX);
[2505]655
656    if((long)regs.RAX < 0)
[2426]657    {
[2505]658        errno = -(long)regs.RAX;
659        perror("syscall");
660        return -1;
[2426]661    }
662
[2505]663    return regs.RAX;
[2426]664}
665
[2509]666/* For debugging purposes only. Prints register and stack information. */
[2511]667#if defined DEBUG
[2509]668static void print_registers(pid_t pid)
[2426]669{
[2509]670    union { long int l; unsigned char data[sizeof(long int)]; } inst;
[2426]671    struct user_regs_struct regs;
[2509]672    int i;
[2426]673
674    if(ptrace(PTRACE_GETREGS, pid, NULL, &regs) < 0)
675    {
[3326]676        perror("PTRACE_GETREGS (syscall 2)");
[2509]677        exit(errno);
[2426]678    }
679
[2509]680    fprintf(stderr, "  / %s: "FMT"   ", STRINGIFY(RAX), regs.RAX);
681    fprintf(stderr, "%s: "FMT"\n", STRINGIFY(RBX), regs.RBX);
682    fprintf(stderr, "  | %s: "FMT"   ", STRINGIFY(RCX), regs.RCX);
683    fprintf(stderr, "%s: "FMT"\n", STRINGIFY(RDX), regs.RDX);
684    fprintf(stderr, "  | %s: "FMT"   ", STRINGIFY(RDI), regs.RDI);
685    fprintf(stderr, "%s: "FMT"\n", STRINGIFY(RSI), regs.RSI);
686    fprintf(stderr, "  | %s: "FMT"   ", STRINGIFY(RSP), regs.RSP);
687    fprintf(stderr, "%s: "FMT"\n", STRINGIFY(RIP), regs.RIP);
[2505]688
[2509]689    inst.l = ptrace(PTRACE_PEEKTEXT, pid, regs.RIP - 4, 0);
690    fprintf(stderr, "  | code: ... %02x %02x %02x %02x <---> ",
691            inst.data[0], inst.data[1], inst.data[2], inst.data[3]);
692    inst.l = ptrace(PTRACE_PEEKTEXT, pid, regs.RIP, 0);
693    fprintf(stderr, "%02x %02x %02x %02x ...\n",
694            inst.data[0], inst.data[1], inst.data[2], inst.data[3]);
[2426]695
[2509]696    fprintf(stderr, "  \\ stack: ... ");
697    for(i = -16; i < 24; i += sizeof(long))
[2426]698    {
[2509]699        inst.l = ptrace(PTRACE_PEEKDATA, pid, regs.RSP + i, 0);
700#if defined __x86_64__
701        fprintf(stderr, "%02x %02x %02x %02x %02x %02x %02x %02x ",
702                inst.data[0], inst.data[1], inst.data[2], inst.data[3],
703                inst.data[4], inst.data[5], inst.data[6], inst.data[7]);
704#else
705        fprintf(stderr, "%02x %02x %02x %02x ",
706                inst.data[0], inst.data[1], inst.data[2], inst.data[3]);
707#endif
708        if(i == 0)
709            fprintf(stderr, "[%s] ", STRINGIFY(RSP));
[2426]710    }
[2509]711    fprintf(stderr, "...\n");
[2426]712}
[2511]713#endif /* DEBUG */
[2426]714
[2511]715#endif /* USE_GRAB */
Note: See TracBrowser for help on using the repository browser.