kernel - Terminate ddb backtraces at Xfast_syscall
[dragonfly.git] / sys / platform / pc64 / x86_64 / db_trace.c
1 /*
2  * Copyright (c) 2008 The DragonFly Project.  All rights reserved.
3  * 
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in
12  *    the documentation and/or other materials provided with the
13  *    distribution.
14  * 3. Neither the name of The DragonFly Project nor the names of its
15  *    contributors may be used to endorse or promote products derived
16  *    from this software without specific, prior written permission.
17  * 
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
22  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
24  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
28  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29  * SUCH DAMAGE.
30  * 
31  * --
32  *
33  * Mach Operating System
34  * Copyright (c) 1991,1990 Carnegie Mellon University
35  * All Rights Reserved.
36  *
37  * Permission to use, copy, modify and distribute this software and its
38  * documentation is hereby granted, provided that both the copyright
39  * notice and this permission notice appear in all copies of the
40  * software, derivative works or modified versions, and any portions
41  * thereof, and that both notices appear in supporting documentation.
42  *
43  * CARNEGIE MELLON ALLOWS FREE USE OF THIS SOFTWARE IN ITS
44  * CONDITION.  CARNEGIE MELLON DISCLAIMS ANY LIABILITY OF ANY KIND FOR
45  * ANY DAMAGES WHATSOEVER RESULTING FROM THE USE OF THIS SOFTWARE.
46  *
47  * Carnegie Mellon requests users of this software to return to
48  *
49  *  Software Distribution Coordinator  or  Software.Distribution@CS.CMU.EDU
50  *  School of Computer Science
51  *  Carnegie Mellon University
52  *  Pittsburgh PA 15213-3890
53  *
54  * any improvements or extensions that they make and grant Carnegie the
55  * rights to redistribute these changes.
56  *
57  * $FreeBSD: src/sys/i386/i386/db_trace.c,v 1.35.2.3 2002/02/21 22:31:25 silby Exp $
58  */
59
60 #include <sys/param.h>
61 #include <sys/systm.h>
62 #include <sys/linker_set.h>
63 #include <sys/lock.h>
64 #include <sys/proc.h>
65 #include <sys/reg.h>
66
67 #include <machine/cpu.h>
68 #include <machine/md_var.h>
69
70 #include <vm/vm.h>
71 #include <vm/vm_param.h>
72 #include <vm/pmap.h>
73 #include <vm/vm_map.h>
74 #include <ddb/ddb.h>
75 #include <dlfcn.h>      /* DLL */
76
77 #include <sys/user.h>
78
79 #include <ddb/db_access.h>
80 #include <ddb/db_sym.h>
81 #include <ddb/db_variables.h>
82
83 db_varfcn_t db_dr0;
84 db_varfcn_t db_dr1;
85 db_varfcn_t db_dr2;
86 db_varfcn_t db_dr3;
87 db_varfcn_t db_dr4;
88 db_varfcn_t db_dr5;
89 db_varfcn_t db_dr6;
90 db_varfcn_t db_dr7;
91
92 /*
93  * Machine register set.
94  */
95 struct db_variable db_regs[] = {
96         { "cs",         &ddb_regs.tf_cs,     NULL },
97 /*      { "ds",         &ddb_regs.tf_ds,     NULL },
98         { "es",         &ddb_regs.tf_es,     NULL },
99         { "fs",         &ddb_regs.tf_fs,     NULL },
100         { "gs",         &ddb_regs.tf_gs,     NULL }, */
101         { "ss",         &ddb_regs.tf_ss,     NULL },
102         { "rax",        &ddb_regs.tf_rax,    NULL },
103         { "rcx",        &ddb_regs.tf_rcx,    NULL },
104         { "rdx",        &ddb_regs.tf_rdx,    NULL },
105         { "rbx",        &ddb_regs.tf_rbx,    NULL },
106         { "rsp",        &ddb_regs.tf_rsp,    NULL },
107         { "rbp",        &ddb_regs.tf_rbp,    NULL },
108         { "rsi",        &ddb_regs.tf_rsi,    NULL },
109         { "rdi",        &ddb_regs.tf_rdi,    NULL },
110         { "rip",        &ddb_regs.tf_rip,    NULL },
111         { "rfl",        &ddb_regs.tf_rflags, NULL },
112         { "r8",         &ddb_regs.tf_r8,     NULL },
113         { "r9",         &ddb_regs.tf_r9,     NULL },
114         { "r10",        &ddb_regs.tf_r10,    NULL },
115         { "r11",        &ddb_regs.tf_r11,    NULL },
116         { "r12",        &ddb_regs.tf_r12,    NULL },
117         { "r13",        &ddb_regs.tf_r13,    NULL },
118         { "r14",        &ddb_regs.tf_r14,    NULL },
119         { "r15",        &ddb_regs.tf_r15,    NULL },
120         { "dr0",        NULL,                db_dr0 },
121         { "dr1",        NULL,                db_dr1 },
122         { "dr2",        NULL,                db_dr2 },
123         { "dr3",        NULL,                db_dr3 },
124         { "dr4",        NULL,                db_dr4 },
125         { "dr5",        NULL,                db_dr5 },
126         { "dr6",        NULL,                db_dr6 },
127         { "dr7",        NULL,                db_dr7 },
128 };
129 struct db_variable *db_eregs = db_regs + NELEM(db_regs);
130
131 /*
132  * Stack trace.
133  */
134 #define INKERNEL(va)    (((vm_offset_t)(va)) >= USRSTACK)
135
136 struct x86_64_frame {
137         struct x86_64_frame     *f_frame;
138         long                    f_retaddr;
139         long                    f_arg0;
140 };
141
142 #define NORMAL          0
143 #define TRAP            1
144 #define INTERRUPT       2
145 #define SYSCALL         3
146
147 static void     db_nextframe(struct x86_64_frame **, db_addr_t *);
148 static int      db_numargs(struct x86_64_frame *);
149 static void     db_print_stack_entry(const char *, int, char **, long *, db_addr_t);
150 static void     dl_symbol_values(long callpc, const char **name);
151
152
153 static char     *watchtype_str(int type);
154 static int      kx86_64_set_watch(int watchnum, unsigned int watchaddr, 
155                                int size, int access, struct dbreg * d);
156 static int      kx86_64_clr_watch(int watchnum, struct dbreg * d);
157 int             db_md_set_watchpoint(db_expr_t addr, db_expr_t size);
158 int             db_md_clr_watchpoint(db_expr_t addr, db_expr_t size);
159 void            db_md_list_watchpoints(void);
160
161
162 /*
163  * Figure out how many arguments were passed into the frame at "fp".
164  */
165 static int
166 db_numargs(struct x86_64_frame *fp)
167 {
168 #if 1
169         return (0);     /* regparm, needs dwarf2 info */
170 #else
171         int     args;
172 #if 0
173         int     *argp;
174         int     inst;
175
176         argp = (int *)db_get_value((int)&fp->f_retaddr, 4, FALSE);
177         /*
178          * XXX etext is wrong for LKMs.  We should attempt to interpret
179          * the instruction at the return address in all cases.  This
180          * may require better fault handling.
181          */
182         if (argp < (int *)btext || argp >= (int *)etext) {
183                 args = 5;
184         } else {
185                 inst = db_get_value((int)argp, 4, FALSE);
186                 if ((inst & 0xff) == 0x59)      /* popl %ecx */
187                         args = 1;
188                 else if ((inst & 0xffff) == 0xc483)     /* addl $Ibs, %esp */
189                         args = ((inst >> 16) & 0xff) / 4;
190                 else
191                         args = 5;
192         }
193 #endif
194         args = 5;
195         return(args);
196 #endif
197 }
198
199 static void
200 db_print_stack_entry(const char *name, int narg, char **argnp, long *argp,
201                      db_addr_t callpc)
202 {
203         db_printf("%s(", name);
204         while (narg) {
205                 if (argnp)
206                         db_printf("%s=", *argnp++);
207                 db_printf("%ld", (long)db_get_value((long)argp, 8, FALSE));
208                 argp++;
209                 if (--narg != 0)
210                         db_printf(",");
211         }
212         db_printf(") at ");
213         db_printsym(callpc, DB_STGY_PROC);
214         db_printf(" %p ",  (void*) callpc);
215         db_printf("\n");
216 }
217
218 /*
219  * Figure out the next frame up in the call stack.
220  */
221 static void
222 db_nextframe(struct x86_64_frame **fp, db_addr_t *ip)
223 {
224         struct trapframe *tf;
225         int frame_type;
226         long rip, rsp, rbp;
227         db_expr_t offset;
228         const char *sym, *name;
229
230         if ((unsigned long)*fp < PAGE_SIZE) {
231                 *fp = NULL;
232                 return;
233         }
234         rip = db_get_value((long) &(*fp)->f_retaddr, 8, FALSE);
235         rbp = db_get_value((long) &(*fp)->f_frame, 8, FALSE);
236
237         /*
238          * Figure out frame type.
239          */
240
241         frame_type = NORMAL;
242
243         sym = db_search_symbol(rip, DB_STGY_ANY, &offset);
244         db_symbol_values(sym, &name, NULL);
245         dl_symbol_values(rip, &name);
246         if (name != NULL) {
247                 if (!strcmp(name, "calltrap")) {
248                         frame_type = TRAP;
249                 } else if (!strncmp(name, "Xresume", 7)) {
250                         frame_type = INTERRUPT;
251                 } else if (!strcmp(name, "_Xsyscall")) {
252                         frame_type = SYSCALL;
253                 }
254         }
255
256         /*
257          * Normal frames need no special processing.
258          */
259         if (frame_type == NORMAL) {
260                 *ip = (db_addr_t) rip;
261                 *fp = (struct x86_64_frame *) rbp;
262                 return;
263         }
264
265         db_print_stack_entry(name, 0, 0, 0, rip);
266
267         /*
268          * Point to base of trapframe which is just above the
269          * current frame.
270          */
271         tf = (struct trapframe *)((long)*fp + 16);
272
273 #if 0
274         rsp = (ISPL(tf->tf_cs) == SEL_UPL) ?  tf->tf_rsp : (long)&tf->tf_rsp;
275 #endif
276         rsp = (long)&tf->tf_rsp;
277
278         switch (frame_type) {
279         case TRAP:
280                 {
281                         rip = tf->tf_rip;
282                         rbp = tf->tf_rbp;
283                         db_printf(
284             "--- trap %016lx, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n",
285                             tf->tf_trapno, rip, rsp, rbp);
286                 }
287                 break;
288         case SYSCALL:
289                 {
290                         rip = tf->tf_rip;
291                         rbp = tf->tf_rbp;
292                         db_printf(
293         "--- syscall %016lx, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n",
294                             tf->tf_rax, rip, rsp, rbp);
295                 }
296                 break;
297         case INTERRUPT:
298                 tf = (struct trapframe *)((long)*fp + 16);
299                 {
300                         rip = tf->tf_rip;
301                         rbp = tf->tf_rbp;
302                         db_printf(
303             "--- interrupt, rip = %016lx, rsp = %016lx, rbp = %016lx ---\n",
304                             rip, rsp, rbp);
305                 }
306                 break;
307         default:
308                 break;
309         }
310
311         *ip = (db_addr_t) rip;
312         *fp = (struct x86_64_frame *) rbp;
313 }
314
315 void
316 db_stack_trace_cmd(db_expr_t addr, boolean_t have_addr, db_expr_t count,
317                    char *modif)
318 {
319         struct x86_64_frame *frame;
320         long *argp;
321         db_addr_t callpc;
322         boolean_t first;
323         int i;
324
325         if (count == -1)
326                 count = 1024;
327
328         if (!have_addr) {
329                 frame = (struct x86_64_frame *)BP_REGS(&ddb_regs);
330                 if (frame == NULL)
331                         frame = (struct x86_64_frame *)(SP_REGS(&ddb_regs) - 8);
332                 callpc = PC_REGS(&ddb_regs);
333         } else {
334                 /*
335                  * Look for something that might be a frame pointer, just as
336                  * a convenience.
337                  */
338                 frame = (struct x86_64_frame *)addr;
339                 for (i = 0; i < 4096; i += 8) {
340                         struct x86_64_frame *check;
341
342                         check = (struct x86_64_frame *)db_get_value((long)((char *)&frame->f_frame + i), 8, FALSE);
343                         if ((char *)check - (char *)frame >= 0 &&
344                             (char *)check - (char *)frame < 4096
345                         ) {
346                                 break;
347                         }
348                         db_printf("%p does not look like a stack frame, skipping\n", (char *)&frame->f_frame + i);
349                 }
350                 if (i == 4096) {
351                         db_printf("Unable to find anything that looks like a stack frame\n");
352                         return;
353                 }
354                 frame = (void *)((char *)frame + i);
355                 db_printf("Trace beginning at frame %p\n", frame);
356                 callpc = (db_addr_t)db_get_value((long)&frame->f_retaddr, 8, FALSE);
357         }
358
359         first = TRUE;
360         while (count--) {
361                 struct x86_64_frame *actframe;
362                 int             narg;
363                 const char *    name;
364                 db_expr_t       offset;
365                 c_db_sym_t      sym;
366 #define MAXNARG 16
367                 char    *argnames[MAXNARG], **argnp = NULL;
368
369                 sym = db_search_symbol(callpc, DB_STGY_ANY, &offset);
370                 db_symbol_values(sym, &name, NULL);
371                 dl_symbol_values(callpc, &name);
372
373                 /*
374                  * Attempt to determine a (possibly fake) frame that gives
375                  * the caller's pc.  It may differ from `frame' if the
376                  * current function never sets up a standard frame or hasn't
377                  * set one up yet or has just discarded one.  The last two
378                  * cases can be guessed fairly reliably for code generated
379                  * by gcc.  The first case is too much trouble to handle in
380                  * general because the amount of junk on the stack depends
381                  * on the pc (the special handling of "calltrap", etc. in
382                  * db_nextframe() works because the `next' pc is special).
383                  */
384                 actframe = frame;
385                 if (first) {
386                         if (!have_addr) {
387                                 int instr;
388
389                                 instr = db_get_value(callpc, 4, FALSE);
390                                 if ((instr & 0xffffffff) == 0xe5894855) {
391                                         /* pushq %rbp; movq %rsp, %rbp */
392                                         actframe = (struct x86_64_frame *)
393                                             (SP_REGS(&ddb_regs) - 8);
394                                 } else if ((instr & 0xffffff) == 0xe58948) {
395                                         /* movq %rsp, %rbp */
396                                         actframe = (struct x86_64_frame *)
397                                             SP_REGS(&ddb_regs);
398                                         if (ddb_regs.tf_rbp == 0) {
399                                                 /* Fake caller's frame better. */
400                                                 frame = actframe;
401                                         }
402                                 } else if ((instr & 0xff) == 0xc3) {
403                                         /* ret */
404                                         actframe = (struct x86_64_frame *)
405                                             (SP_REGS(&ddb_regs) - 8);
406                                 } else if (offset == 0) {
407                                         /* Probably a symbol in assembler code. */
408                                         actframe = (struct x86_64_frame *)
409                                             (SP_REGS(&ddb_regs) - 8);
410                                 }
411                         } else if (name != NULL &&
412                                    strcmp(name, "fork_trampoline") == 0) {
413                                 /*
414                                  * Don't try to walk back on a stack for a
415                                  * process that hasn't actually been run yet.
416                                  */
417                                 db_print_stack_entry(name, 0, 0, 0, callpc);
418                                 break;
419                         }
420                         first = FALSE;
421                 }
422
423                 argp = &actframe->f_arg0;
424                 narg = MAXNARG;
425                 if (sym != NULL && db_sym_numargs(sym, &narg, argnames)) {
426                         argnp = argnames;
427                 } else {
428                         narg = db_numargs(frame);
429                 }
430
431                 db_print_stack_entry(name, narg, argnp, argp, callpc);
432
433                 /*
434                  * Stop at the system call boundary (else we risk
435                  * double-faulting on junk).
436                  */
437                 if (name && strcmp(name, "Xfast_syscall") == 0)
438                         break;
439
440                 if (actframe != frame) {
441                         /* `frame' belongs to caller. */
442                         callpc = (db_addr_t)
443                             db_get_value((long)&actframe->f_retaddr, 8, FALSE);
444                         continue;
445                 }
446
447                 db_nextframe(&frame, &callpc);
448                 if (frame == NULL)
449                         break;
450         }
451 }
452
453 void
454 print_backtrace(int count)
455 {
456         register_t  rbp;
457
458         __asm __volatile("movq %%rbp, %0" : "=r" (rbp));
459         db_stack_trace_cmd(rbp, 1, count, NULL);
460 }
461
462 #define DB_DRX_FUNC(reg)                                                \
463 int                                                                     \
464 db_ ## reg (struct db_variable *vp, db_expr_t *valuep, int op)          \
465 {                                                                       \
466         if (op == DB_VAR_GET)                                           \
467                 *valuep = r ## reg ();                                  \
468         else                                                            \
469                 load_ ## reg (*valuep);                                 \
470                                                                         \
471         return(0);                                                      \
472
473
474 DB_DRX_FUNC(dr0)
475 DB_DRX_FUNC(dr1)
476 DB_DRX_FUNC(dr2)
477 DB_DRX_FUNC(dr3)
478 DB_DRX_FUNC(dr4)
479 DB_DRX_FUNC(dr5)
480 DB_DRX_FUNC(dr6)
481 DB_DRX_FUNC(dr7)
482
483 static int
484 kx86_64_set_watch(int watchnum, unsigned int watchaddr, int size, int access,
485                struct dbreg *d)
486 {
487         int i;
488         unsigned int mask;
489         
490         if (watchnum == -1) {
491                 for (i = 0, mask = 0x3; i < 4; i++, mask <<= 2)
492                         if ((d->dr[7] & mask) == 0)
493                                 break;
494                 if (i < 4)
495                         watchnum = i;
496                 else
497                         return(-1);
498         }
499
500         switch (access) {
501         case DBREG_DR7_EXEC:
502                 size = 1; /* size must be 1 for an execution breakpoint */
503                 /* fall through */
504         case DBREG_DR7_WRONLY:
505         case DBREG_DR7_RDWR:
506                 break;
507         default:
508                 return(-1);
509         }
510
511         /*
512          * we can watch a 1, 2, 4, or 8 byte sized location
513          */
514         switch (size) {
515         case 1:
516                 mask = 0x00;
517                 break;
518         case 2:
519                 mask = 0x01 << 2;
520                 break;
521         case 4:
522                 mask = 0x03 << 2;
523                 break;
524         case 8:
525                 mask = 0x02 << 2;
526                 break;
527         default:
528                 return(-1);
529         }
530
531         mask |= access;
532
533         /* clear the bits we are about to affect */
534         d->dr[7] &= ~((0x3 << (watchnum * 2)) | (0x0f << (watchnum * 4 + 16)));
535
536         /* set drN register to the address, N=watchnum */
537         DBREG_DRX(d, watchnum) = watchaddr;
538
539         /* enable the watchpoint */
540         d->dr[7] |= (0x2 << (watchnum * 2)) | (mask << (watchnum * 4 + 16));
541
542         return(watchnum);
543 }
544
545
546 int
547 kx86_64_clr_watch(int watchnum, struct dbreg *d)
548 {
549         if (watchnum < 0 || watchnum >= 4)
550                 return(-1);
551
552         d->dr[7] &= ~((0x3 << (watchnum * 2)) | (0x0f << (watchnum * 4 + 16)));
553         DBREG_DRX(d, watchnum) = 0;
554
555         return(0);
556 }
557
558
559 int
560 db_md_set_watchpoint(db_expr_t addr, db_expr_t size)
561 {
562         int avail, wsize;
563         int i;
564         struct dbreg d;
565         
566         fill_dbregs(NULL, &d);
567
568         avail = 0;
569         for (i = 0; i < 4; i++) {
570                 if ((d.dr[7] & (3 << (i * 2))) == 0)
571                         avail++;
572         }
573
574         if (avail * 8 < size)
575                 return(-1);
576
577         for (i=0; i < 4 && (size != 0); i++) {
578                 if ((d.dr[7] & (3 << (i * 2))) == 0) {
579                         if (size >= 8 || (avail == 1 && size > 4))
580                                 wsize = 8;
581                         else if (size > 2)
582                                 wsize = 4;
583                         else
584                                 wsize = size;
585                         if (wsize == 3)
586                                 wsize++;
587                         kx86_64_set_watch(i, addr, wsize, DBREG_DR7_WRONLY, &d);
588                         addr += wsize;
589                         size -= wsize;
590                 }
591         }
592
593         set_dbregs(NULL, &d);
594
595         return(0);
596 }
597
598 int
599 db_md_clr_watchpoint(db_expr_t addr, db_expr_t size)
600 {
601         struct dbreg d;
602         int i;
603
604         fill_dbregs(NULL, &d);
605
606         for(i = 0; i < 4; i++) {
607                 if (d.dr[7] & (3 << (i * 2))) {
608                         if ((DBREG_DRX((&d), i) >= addr) && 
609                             (DBREG_DRX((&d), i) < addr + size))
610                                 kx86_64_clr_watch(i, &d);
611                 }
612         }
613
614         set_dbregs(NULL, &d);
615
616         return(0);
617 }
618
619 static char *
620 watchtype_str(int type)
621 {
622         switch (type) {
623         case DBREG_DR7_EXEC:
624                 return "execute";
625         case DBREG_DR7_RDWR:
626                 return "read/write";
627         case DBREG_DR7_WRONLY:
628                 return "write";
629         default:
630                 return "invalid";
631         }
632 }
633
634 void
635 db_md_list_watchpoints(void)
636 {
637         int i;
638         struct dbreg d;
639
640         fill_dbregs(NULL, &d);
641
642         db_printf("\nhardware watchpoints:\n");
643         db_printf("  watch    status        type  len     address\n"
644                   "  -----  --------  ----------  ---  ----------\n");
645         for (i = 0; i < 4; i++) {
646                 if (d.dr[7] & (0x03 << (i * 2))) {
647                         unsigned type, len;
648                         type = (d.dr[7] >> (16 + (i * 4))) & 3;
649                         len =  (d.dr[7] >> (16 + (i * 4) + 2)) & 3;
650                         db_printf("  %-5d  %-8s  %10s  %3d  0x%08lx\n",
651                                   i, "enabled", watchtype_str(type), 
652                                   len + 1, DBREG_DRX((&d), i));
653                 } else {
654                         db_printf("  %-5d  disabled\n", i);
655                 }
656         }
657
658         db_printf("\ndebug register values:\n");
659         for (i = 0; i < 8; i++)
660                 db_printf("  dr%d 0x%08lx\n", i, DBREG_DRX((&d),i));
661         db_printf("\n");
662 }
663
664 /*
665  * See if dladdr() can get the symbol name via the standard dynamic loader.
666  */
667 static
668 void
669 dl_symbol_values(long callpc, const char **name)
670 {
671 #if 0
672         Dl_info info;
673         if (*name == NULL) {
674                 if (dladdr((const void *)callpc, &info) != 0) {
675                         if (info.dli_saddr <= (const void *)callpc)
676                                 *name = info.dli_sname;
677                 }
678         }
679 #endif
680 }
681