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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
|
/* trace(1) - the MINIX3 system call tracer - by D.C. van Moolenbroek */
#include "inc.h"
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include <err.h>
/* Global variables, used only for a subset of the command line options. */
int timestamps; /* 0 = none, 1 = time w/o usecs, 2 = time w/usecs */
int allnames; /* FALSE = structure field names, TRUE = all names */
unsigned int valuesonly; /* 0 = normal, 1 = no symbols, 2 = no structures */
unsigned int verbose; /* 0 = essentials, 1 = elaborate, 2 = everything */
/* Local variables, for signal handling. */
static int got_signal, got_info;
/*
* Signal handler for signals that are supposed to make us terminate. Let the
* main loop do the actual work, since it might be in the middle of processing
* a process status change right now.
*/
static void
sig_handler(int __unused sig)
{
got_signal = TRUE;
}
/*
* Signal handler for the SIGINFO signal. Let the main loop report on all
* processes currenty being traced. Since SIGINFO is sent to the current
* process group, traced children may get the signal as well. This is both
* intentional and impossible to prevent.
*/
static void
info_handler(int __unused sig)
{
got_info = TRUE;
}
/*
* Print a list of traced processes and their call status. We must not
* interfere with actual process output, so perform out-of-band printing
* (with info lines rather than lines prefixed by each process's PID).
*/
static void
list_info(void)
{
struct trace_proc *proc;
int no_call, in_call;
put_newline();
for (proc = proc_next(NULL); proc != NULL; proc = proc_next(proc)) {
/*
* When attaching to an existing process, there is no way to
* find out whether the process is in a system call or not.
*/
no_call = (proc->trace_flags & TF_NOCALL);
in_call = (proc->trace_flags & TF_INCALL);
assert(!in_call || !no_call);
put_fmt(NULL, "Tracing %s (pid %d), %s%s%s", proc->name,
proc->pid, no_call ? "call status unknown" :
(in_call ? "in a " : "not in a call"),
in_call ? call_name(proc) : "",
in_call ? " call" : "");
put_newline();
}
}
/*
* Either we have just started or attached to the given process, it the process
* has performed a successful execve() call. Obtain the new process name, and
* print a banner for it.
*/
static void
new_exec(struct trace_proc * proc)
{
/* Failure to obtain the process name is worrisome, but not fatal.. */
if (kernel_get_name(proc->pid, proc->name, sizeof(proc->name)) < 0)
strlcpy(proc->name, "<unknown>", sizeof(proc->name));
put_newline();
put_fmt(proc, "Tracing %s (pid %d)", proc->name, proc->pid);
put_newline();
}
/*
* We have started or attached to a process. Set the appropriate flags, and
* print a banner showing that we are now tracing it.
*/
static void
new_proc(struct trace_proc * proc, int follow_fork)
{
int fl;
/* Set the desired tracing options. */
fl = TO_ALTEXEC;
if (follow_fork) fl |= TO_TRACEFORK;
(void)ptrace(T_SETOPT, proc->pid, 0, fl);
/*
* When attaching to an arbitrary process, this process might be in the
* middle of an execve(). Now that we have enabled TO_ALTEXEC, we may
* now get a SIGSTOP signal next. Guard against this by marking the
* first system call as a possible execve().
*/
if ((proc->trace_flags & (TF_ATTACH | TF_STOPPING)) == TF_ATTACH)
proc->trace_flags |= TF_EXEC;
new_exec(proc);
}
/*
* A process has terminated or is being detached. Print the resulting status.
*/
static void
discard_proc(struct trace_proc * proc, int status)
{
const char *signame;
/*
* The exit() calls are of type no-return, meaning they are expected
* not to return. However, calls of this type may in fact return an
* error, in which case the error must be printed. Thus, such calls
* are not actually finished until the end of the call-leave phase.
* For exit() calls, a successful call will never get to the call-leave
* phase. The result is that such calls will end up being shown as
* suspended, which is unintuitive. To counter this, we pretend that a
* clean process exit is in fact preceded by a call-leave event, thus
* allowing the call to be printed without suspension. An example:
*
* 3| exit(0) <..>
* 2| setsid() = 2
* [A] 3| exit(0)
* 3| Process exited normally with code 0
*
* The [A] line is the result of the following code.
*/
if (WIFEXITED(status) && (proc->trace_flags & TF_INCALL))
call_leave(proc, TRUE /*skip*/);
put_newline();
if (WIFEXITED(status)) {
put_fmt(proc, "Process exited normally with code %d",
WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
if ((signame = get_signal_name(WTERMSIG(status))) != NULL)
put_fmt(proc, "Process terminated from signal %s",
signame);
else
put_fmt(proc, "Process terminated from signal %d",
WTERMSIG(status));
} else if (WIFSTOPPED(status))
put_text(proc, "Process detached");
else
put_fmt(proc, "Bogus wait result (%04x)", status);
put_newline();
proc_del(proc);
}
/*
* The given process has been stopped on a system call, either entering or
* leaving that call.
*/
static void
handle_call(struct trace_proc * proc, int show_stack)
{
reg_t pc, sp;
int class, skip, new_ctx;
proc->trace_flags &= ~TF_NOCALL;
if (proc->trace_flags & TF_SKIP) {
/* Skip the call leave phase after a successful execve(). */
proc->trace_flags &= ~(TF_INCALL | TF_SKIP);
} else if (!(proc->trace_flags & TF_INCALL)) {
/*
* The call_enter call returns the class of the call:
* TC_NORMAL, TC_EXEC, or TC_SIGRET. TC_EXEC means that an
* execve() call is being performed. This means that if a
* SIGSTOP follows for the current process, the process has
* successfully started a different executable. TC_SIGRET
* means that if successful, the call will have a bogus return
* value. TC_NORMAL means that the call requires no exception.
*/
class = call_enter(proc, show_stack);
switch (class) {
case TC_NORMAL:
break;
case TC_EXEC:
proc->trace_flags |= TF_EXEC;
break;
case TC_SIGRET:
proc->trace_flags |= TF_CTX_SKIP;
break;
default:
assert(0);
}
/* Save the current program counter and stack pointer. */
if (!kernel_get_context(proc->pid, &pc, &sp, NULL /*fp*/)) {
proc->last_pc = pc;
proc->last_sp = sp;
} else
proc->last_pc = proc->last_sp = 0;
proc->trace_flags |= TF_INCALL;
} else {
/*
* Check if the program counter or stack pointer have changed
* during the system call. If so, this is a strong indication
* that a sigreturn call has succeeded, and thus its result
* must be skipped, since the result register will not contain
* the result of the call.
*/
new_ctx = (proc->last_pc != 0 &&
!kernel_get_context(proc->pid, &pc, &sp, NULL /*fp*/) &&
(pc != proc->last_pc || sp != proc->last_sp));
skip = ((proc->trace_flags & TF_CTX_SKIP) && new_ctx);
call_leave(proc, skip);
/*
* On such context changes, also print a short dashed line.
* This helps in identifying signal handler invocations,
* although it is not reliable for that purpose: no dashed line
* will be printed if a signal handler is invoked while the
* process is not making a system call.
*/
if (new_ctx) {
put_text(proc, "---");
put_newline();
}
proc->trace_flags &= ~(TF_INCALL | TF_CTX_SKIP | TF_EXEC);
}
}
/*
* The given process has received the given signal. Report the receipt. Due
* to the way that signal handling with traced processes works, the signal may
* in fact be delivered to the process much later, or never--a problem inherent
* to the way signals are handled in PM right now (namely, deferring signal
* delivery would let the traced process block signals meant for the tracer).
*/
static void
report_signal(struct trace_proc * proc, int sig, int show_stack)
{
const char *signame;
/*
* Print a stack trace only if we are not in a call; otherwise, we
* would simply get the same stack trace twice and mess up the output
* in the process, because call suspension is not expected if we are
* tracing a single process only.
* FIXME: the check should be for whether we actually print the call..
*/
if (show_stack && !(proc->trace_flags & TF_INCALL))
kernel_put_stacktrace(proc);
/*
* If this process is in the middle of a call, the signal will be
* printed within the call. This will always happen on the call split,
* that is, between the call's entering (out) and leaving (in) phases.
* This also means that the recording of the call-enter phase may be
* replayed more than once, and the call may be suspended more than
* once--after all, a signal is not necessarily followed immediately
* by the call result. If the process is not in the middle of a call,
* the signal will end up on a separate line. In both cases, multiple
* consecutive signals may be printed right after one another. The
* following scenario shows a number of possible combinations:
*
* 2| foo(<..>
* 3| ** SIGHUP ** ** SIGUSR1 **
* 3| bar() = <..>
* 2|*foo(** SIGUSR1 ** ** SIGUSR2 ** <..>
* 3|*bar() = ** SIGCHLD ** 0
* 2|*foo(** SIGINT ** &0xef852000) = -1 [EINTR]
* 3| kill(3, SIGTERM) = ** SIGTERM ** <..>
* 3| Process terminated from signal SIGTERM
*/
call_replay(proc);
if (!valuesonly && (signame = get_signal_name(sig)) != NULL)
put_fmt(proc, "** %s **", signame);
else
put_fmt(proc, "** SIGNAL %d **", sig);
put_space(proc);
output_flush();
}
/*
* Wait for the given process ID to stop on the given signal. Upon success,
* the function will return zero. Upon failure, it will return -1, and errno
* will be either set to an error code, or to zero in order to indicate that
* the process exited instead.
*/
static int
wait_sig(pid_t pid, int sig)
{
int status;
for (;;) {
if (waitpid(pid, &status, 0) == -1) {
if (errno == EINTR) continue;
return -1;
}
if (!WIFSTOPPED(status)) {
/* The process terminated just now. */
errno = 0;
return -1;
}
if (WSTOPSIG(status) == sig)
break;
(void)ptrace(T_RESUME, pid, 0, WSTOPSIG(status));
}
return 0;
}
/*
* Attach to the given process, and wait for the resulting SIGSTOP signal.
* Other signals may arrive first; we pass these on to the process without
* reporting them, thus logically modelling them as having arrived before we
* attached to the process. The process might also exit in the meantime,
* typically as a result of a lethal signal; following the same logical model,
* we pretend the process did not exist in the first place. Since the SIGSTOP
* signal will be pending right after attaching to the process, this procedure
* will never block.
*/
static int
attach(pid_t pid)
{
if (ptrace(T_ATTACH, pid, 0, 0) != 0) {
warn("Unable to attach to pid %d", pid);
return -1;
}
if (wait_sig(pid, SIGSTOP) != 0) {
/* If the process terminated, report it as not found. */
if (errno == 0)
errno = ESRCH;
warn("Unable to attach to pid %d", pid);
return -1;
}
/* Verify that we can read values from the kernel at all. */
if (kernel_check(pid) == FALSE) {
(void)ptrace(T_DETACH, pid, 0, 0);
warnx("Kernel magic check failed, recompile trace(1)");
return -1;
}
/*
* System services are managed by RS, which prevents them from
* being traced properly by PM. Attaching to a service could
* therefore cause problems, so we should detach immediately.
*/
if (kernel_is_service(pid) == TRUE) {
(void)ptrace(T_DETACH, pid, 0, 0);
warnx("Cannot attach to system services!");
return -1;
}
return 0;
}
/*
* Detach from all processes, knowning that they were all processes to which we
* attached explicitly (i.e., not started by us) and are all currently stopped.
*/
static void
detach_stopped(void)
{
struct trace_proc *proc;
for (proc = proc_next(NULL); proc != NULL; proc = proc_next(proc))
(void)ptrace(T_DETACH, proc->pid, 0, 0);
}
/*
* Start detaching from all processes to which we previously attached. The
* function is expected to return before detaching is completed, and the caller
* must deal with the new situation appropriately. Do not touch any processes
* started by us (to allow graceful termination), unless force is set, in which
* case those processes are killed.
*/
static void
detach_running(int force)
{
struct trace_proc *proc;
for (proc = proc_next(NULL); proc != NULL; proc = proc_next(proc)) {
if (proc->trace_flags & TF_ATTACH) {
/* Already detaching? Then do nothing. */
if (proc->trace_flags & TF_DETACH)
continue;
if (!(proc->trace_flags & TF_STOPPING))
(void)kill(proc->pid, SIGSTOP);
proc->trace_flags |= TF_DETACH | TF_STOPPING;
} else {
/*
* The child processes may be ignoring SIGINTs, so upon
* the second try, force them to terminate.
*/
if (force)
(void)kill(proc->pid, SIGKILL);
}
}
}
/*
* Print command usage.
*/
static void __dead
usage(void)
{
(void)fprintf(stderr, "usage: %s [-fgNstVv] [-o file] [-p pid] "
"[command]\n", getprogname());
exit(EXIT_FAILURE);
}
/*
* The main function of the system call tracer.
*/
int
main(int argc, char * argv[])
{
struct trace_proc *proc;
const char *output_file;
int status, sig, follow_fork, show_stack, grouping, first_signal;
pid_t pid, last_pid;
int c, error;
setprogname(argv[0]);
proc_init();
follow_fork = FALSE;
show_stack = FALSE;
grouping = FALSE;
output_file = NULL;
timestamps = 0;
allnames = FALSE;
verbose = 0;
valuesonly = 0;
while ((c = getopt(argc, argv, "fgNstVvo:p:")) != -1) {
switch (c) {
case 'f':
follow_fork = TRUE;
break;
case 'g':
grouping = TRUE;
break;
case 'N':
allnames = TRUE;
break;
case 's':
show_stack = TRUE;
break;
case 't':
timestamps++;
break;
case 'V':
valuesonly++;
break;
case 'v':
verbose++;
break;
case 'o':
output_file = optarg;
break;
case 'p':
pid = atoi(optarg);
if (pid <= 0)
usage();
if (proc_get(pid) == NULL && proc_add(pid) == NULL)
err(EXIT_FAILURE, NULL);
break;
default:
usage();
}
}
argv += optind;
argc -= optind;
first_signal = TRUE;
got_signal = FALSE;
got_info = FALSE;
signal(SIGINT, sig_handler);
signal(SIGINFO, info_handler);
/* Attach to any processes for which PIDs were given. */
for (proc = proc_next(NULL); proc != NULL; proc = proc_next(proc)) {
if (attach(proc->pid) != 0) {
/*
* Detach from the processes that we have attached to
* so far, i.e. the ones with the TF_ATTACH flag.
*/
detach_stopped();
return EXIT_FAILURE;
}
proc->trace_flags = TF_ATTACH | TF_NOCALL;
}
/* If a command is given, start a child that executes the command. */
if (argc >= 1) {
pid = fork();
switch (pid) {
case -1:
warn("Unable to fork");
detach_stopped();
return EXIT_FAILURE;
case 0:
(void)ptrace(T_OK, 0, 0, 0);
(void)execvp(argv[0], argv);
err(EXIT_FAILURE, "Unable to start %s", argv[0]);
default:
break;
}
/*
* The first signal will now be SIGTRAP from the execvp(),
* unless that fails, in which case the child will terminate.
*/
if (wait_sig(pid, SIGTRAP) != 0) {
/*
* If the child exited, the most likely cause is a
* failure to execute the command. Let the child
* report the error, and do not say anything here.
*/
if (errno != 0)
warn("Unable to start process");
detach_stopped();
return EXIT_FAILURE;
}
/* If we haven't already, perform the kernel magic check. */
if (proc_count() == 0 && kernel_check(pid) == FALSE) {
warnx("Kernel magic check failed, recompile trace(1)");
(void)kill(pid, SIGKILL);
detach_stopped();
return EXIT_FAILURE;
}
if ((proc = proc_add(pid)) == NULL) {
warn(NULL);
(void)kill(pid, SIGKILL);
detach_stopped();
return EXIT_FAILURE;
}
proc->trace_flags = 0;
} else
pid = -1;
/* The user will have to give us at least one process to trace. */
if (proc_count() == 0)
usage();
/*
* Open an alternative output file if needed. After that, standard
* error should no longer be used directly, and all output has to go
* through the output module.
*/
if (output_init(output_file) < 0) {
warn("Unable to open output file");
if (pid > 0)
(void)kill(pid, SIGKILL);
detach_stopped();
return EXIT_FAILURE;
}
/*
* All the traced processes are currently stopped. Initialize, report,
* and resume them.
*/
for (proc = proc_next(NULL); proc != NULL; proc = proc_next(proc)) {
new_proc(proc, follow_fork);
(void)ptrace(T_SYSCALL, proc->pid, 0, 0);
}
/*
* Handle events until there are no traced processes left.
*/
last_pid = 0;
error = FALSE;
for (;;) {
/* If an output error occurred, exit as soon as possible. */
if (!error && output_error()) {
detach_running(TRUE /*force*/);
error = TRUE;
}
/*
* If the user pressed ^C once, start detaching the processes
* that we did not start, if any. If the user pressed ^C
* twice, kill the process that we did start, if any.
*/
if (got_signal) {
detach_running(!first_signal);
got_signal = FALSE;
first_signal = FALSE;
}
/* Upon getting SIGINFO, print a list of traced processes. */
if (got_info) {
list_info();
got_info = FALSE;
}
/*
* Block until something happens to a traced process. If
* enabled from the command line, first try waiting for the
* last process for which we got results, so as to reduce call
* suspensions a bit.
*/
if (grouping && last_pid > 0 &&
waitpid(last_pid, &status, WNOHANG) > 0)
pid = last_pid;
else
if ((pid = waitpid(-1, &status, 0)) <= 0) {
if (pid == -1 && errno == EINTR) continue;
if (pid == -1 && errno == ECHILD) break; /* all done */
put_fmt(NULL, "Unexpected waitpid failure: %s",
(pid == 0) ? "No result" : strerror(errno));
put_newline();
/*
* We need waitpid to function correctly in order to
* detach from any attached processes, so we can do
* little more than just exit, effectively killing all
* traced processes.
*/
return EXIT_FAILURE;
}
last_pid = 0;
/* Get the trace data structure for the process. */
if ((proc = proc_get(pid)) == NULL) {
/*
* The waitpid() call returned the status of a process
* that we have not yet seen. This must be a newly
* forked child. If it is not stopped, it must have
* died immediately, and we choose not to report it.
*/
if (!WIFSTOPPED(status))
continue;
if ((proc = proc_add(pid)) == NULL) {
put_fmt(NULL,
"Error attaching to new child %d: %s",
pid, strerror(errno));
put_newline();
/*
* Out of memory allocating a new child object!
* We can not trace this child, so just let it
* run free by detaching from it.
*/
if (WSTOPSIG(status) != SIGSTOP) {
(void)ptrace(T_RESUME, pid, 0,
WSTOPSIG(status));
if (wait_sig(pid, SIGSTOP) != 0)
continue; /* it died.. */
}
(void)ptrace(T_DETACH, pid, 0, 0);
continue;
}
/*
* We must specify TF_ATTACH here, even though it may
* be a child of a process we started, in which case it
* should be killed when we exit. We do not keep track
* of ancestry though, so better safe than sorry.
*/
proc->trace_flags = TF_ATTACH | TF_STOPPING;
new_proc(proc, follow_fork);
/* Repeat entering the fork call for the child. */
handle_call(proc, show_stack);
}
/* If the process died, report its status and clean it up. */
if (!WIFSTOPPED(status)) {
discard_proc(proc, status);
continue;
}
sig = WSTOPSIG(status);
if (sig == SIGSTOP && (proc->trace_flags & TF_STOPPING)) {
/* We expected the process to be stopped; now it is. */
proc->trace_flags &= ~TF_STOPPING;
if (proc->trace_flags & TF_DETACH) {
if (ptrace(T_DETACH, proc->pid, 0, 0) == 0)
discard_proc(proc, status);
/*
* If detaching failed, the process must have
* died, and we'll get notified through wait().
*/
continue;
}
sig = 0;
} else if (sig == SIGSTOP && (proc->trace_flags & TF_EXEC)) {
/* The process has performed a successful execve(). */
call_leave(proc, TRUE /*skip*/);
put_text(proc, "---");
new_exec(proc);
/*
* A successful execve() has no result, in the sense
* that there is no reply message. We should therefore
* not even try to copy in the reply message from the
* original location, because it will be invalid.
* Thus, we skip the exec's call leave phase entirely.
*/
proc->trace_flags &= ~TF_EXEC;
proc->trace_flags |= TF_SKIP;
sig = 0;
} else if (sig == SIGTRAP) {
/* The process is entering or leaving a system call. */
if (!(proc->trace_flags & TF_DETACH))
handle_call(proc, show_stack);
sig = 0;
} else {
/* The process has received a signal. */
report_signal(proc, sig, show_stack);
/*
* Only in this case do we pass the signal to the
* traced process.
*/
}
/*
* Resume process execution. If this call fails, the process
* has probably died. We will find out soon enough.
*/
(void)ptrace(T_SYSCALL, proc->pid, 0, sig);
last_pid = proc->pid;
}
return (error) ? EXIT_FAILURE : EXIT_SUCCESS;
}
|