2 * V8 DTrace ustack helper for annotating native stack traces with JavaScript
3 * function names. We start with a frame pointer (arg1) and emit a string
4 * describing the current function. We do this by chasing pointers to extract
5 * the function's name (if any) and the filename and line number where the
8 * To use the helper, run node, then use the jstack() DTrace action to capture
9 * a JavaScript stacktrace. You may need to tune the dtrace_helper_actions_max
10 * kernel variable to 128.
13 #include <v8constants.h>
17 * V8 represents small integers (SMI) using the upper 31 bits of a 32/64-bit
18 * value. To extract the actual integer value, we must shift it over.
20 #define IS_SMI(value) \
21 ((value & V8_SmiTagMask) == V8_SmiTag)
22 #define SMI_VALUE(value) \
23 ((uint32_t) ((value) >> V8_SmiValueShift))
26 * Heap objects usually start off with a Map pointer, itself another heap
27 * object. However, during garbage collection, the low order bits of the
28 * pointer (which are normally 01) are used to record GC state. Of course, we
29 * have no idea if we're in GC or not, so we must always normalize the pointer.
31 #define V8_MAP_PTR(ptr) \
32 ((ptr & ~V8_HeapObjectTagMask) | V8_HeapObjectTag)
34 #define V8_TYPE_SCRIPT(type) \
35 ((type) == V8_IT_SCRIPT)
38 * Determine the encoding and representation of a V8 string.
40 #define V8_TYPE_STRING(type) \
41 (((type) & V8_IsNotStringMask) == V8_StringTag)
43 #define V8_STRENC_ASCII(type) \
44 (((type) & V8_StringEncodingMask) == V8_AsciiStringTag)
46 #define V8_STRREP_SEQ(type) \
47 (((type) & V8_StringRepresentationMask) == V8_SeqStringTag)
48 #define V8_STRREP_CONS(type) \
49 (((type) & V8_StringRepresentationMask) == V8_ConsStringTag)
50 #define V8_STRREP_EXT(type) \
51 (((type) & V8_StringRepresentationMask) == V8_ExternalStringTag)
54 * String type predicates
56 #define ASCII_SEQSTR(value) \
57 (V8_TYPE_STRING(value) && V8_STRENC_ASCII(value) && V8_STRREP_SEQ(value))
59 #define TWOBYTE_SEQSTR(value) \
60 (V8_TYPE_STRING(value) && !V8_STRENC_ASCII(value) && V8_STRREP_SEQ(value))
62 #define IS_CONSSTR(value) \
63 (V8_TYPE_STRING(value) && V8_STRREP_CONS(value))
65 #define ASCII_EXTSTR(value) \
66 (V8_TYPE_STRING(value) && V8_STRENC_ASCII(value) && V8_STRREP_EXT(value))
69 * General helper macros
71 #define COPYIN_UINT8(addr) (*(uint8_t*) copyin((addr), sizeof(uint8_t)))
72 #define COPYIN_UINT32(addr) (*(uint32_t*) copyin((addr), sizeof(uint32_t)))
73 #define COPYIN_UINT64(addr) (*(uint64_t*) copyin((addr), sizeof(uint64_t)))
76 # define COPYIN_PTR(addr) COPYIN_UINT32(addr)
77 # define off_t uint32_t
78 # define APPEND_PTR(p) APPEND_PTR_32(p)
80 # define COPYIN_PTR(addr) COPYIN_UINT64(addr)
81 # define off_t uint64_t
82 # define APPEND_PTR(p) APPEND_PTR_64(p)
85 #define APPEND_CHR(c) (this->buf[this->off++] = (c))
86 #define APPEND_CHR4(s0, s1, s2, s3) \
91 #define APPEND_CHR8(s0, s1, s2, s3, s4, s5, s6, s7) \
92 APPEND_CHR4(s0, s1, s2, s3) \
93 APPEND_CHR4(s4, s5, s6, s7)
95 #define APPEND_DGT(i, d) \
96 (((i) / (d)) ? APPEND_CHR('0' + ((i)/(d) % 10)) : 0)
98 #define APPEND_NUM(i) \
99 APPEND_DGT((i), 100000); \
100 APPEND_DGT((i), 10000); \
101 APPEND_DGT((i), 1000); \
102 APPEND_DGT((i), 100); \
103 APPEND_DGT((i), 10); \
106 #define APPEND_HEX(d) \
107 APPEND_CHR((d) < 10 ? '0' + (d) : 'a' - 10 + (d))
109 #define APPEND_PTR_32(p) \
110 APPEND_HEX((p >> 28) & 0xf); \
111 APPEND_HEX((p >> 24) & 0xf); \
112 APPEND_HEX((p >> 20) & 0xf); \
113 APPEND_HEX((p >> 16) & 0xf); \
114 APPEND_HEX((p >> 12) & 0xf); \
115 APPEND_HEX((p >> 8) & 0xf); \
116 APPEND_HEX((p >> 4) & 0xf); \
117 APPEND_HEX((p) & 0xf);
119 #define APPEND_PTR_64(p) \
120 APPEND_PTR_32(p >> 32) \
124 * The following macros are used to output ASCII SeqStrings, ConsStrings, and
125 * Node.js ExternalStrings. To represent each string, we use three fields:
127 * "str": a pointer to the string itself
129 * "len": the string length
131 * "attrs": the type identifier for the string, which indicates the
132 * encoding and representation. We're only interested in strings
133 * whose representation is one of:
135 * SeqOneByteString stored directly as a char array inside the object
137 * SeqTwoByteString stored as a UTF-16 char array inside the object
139 * ConsString pointer to two strings that should be concatenated
141 * ExternalString pointer to a char* outside the V8 heap
145 * Load "len" and "attrs" for the given "str".
147 #define LOAD_STRFIELDS(str, len, attrs) \
148 len = SMI_VALUE(COPYIN_PTR(str + V8_OFF_STR_LENGTH)); \
149 this->map = V8_MAP_PTR(COPYIN_PTR(str + V8_OFF_HEAPOBJ_MAP)); \
150 attrs = COPYIN_UINT8(this->map + V8_OFF_MAP_ATTRS);
152 #define APPEND_SEQSTR(str, len, attrs) \
153 APPEND_SEQONEBYTESTR(str, len, attrs) \
154 APPEND_SEQTWOBYTESTR(str, len, attrs)
157 * Print out the given SeqOneByteString, or do nothing if the string is not an ASCII
160 #define APPEND_SEQONEBYTESTR(str, len, attrs) \
161 dtrace:helper:ustack: \
162 /!this->done && len > 0 && ASCII_SEQSTR(attrs)/ \
164 copyinto(str + V8_OFF_STR_CHARS, len, this->buf + this->off); \
169 * LOOP_ITER: macro to paste "block" while "ivar" is less than "dynmax" and
170 * "statmax". The subsequent LOOP_{4,8} macros facilitate pasting the same
171 * thing 4 and 8 times, respectively. Like much of the rest of the code in this
172 * file, this is regrettably necessary given the constraints under which we're
175 #define LOOP_ITER(ivar, dynmax, statmax, block) \
176 ((ivar) < (dynmax)) && ((ivar) < (statmax)) && (block); (ivar)++;
178 #define LOOP_4(block) \
184 #define LOOP_8(block) \
189 * Print out the given SeqTwoByteString, or do nothing if the string is not an ASCII
190 * SeqTwoByteString. NOTE: if you bump MAX_TWOBYTESTR_CHARS, you'll also need
191 * to modify the LOOP_* macro calls below to match.
193 #define MAX_TWOBYTESTR_CHARS 128
194 #define MAX_TWOBYTESTR_BYTES (2 * MAX_TWOBYTESTR_CHARS)
195 #define TO_ASCII(c) ((c) < 128 ? (c) : '?')
197 #define APPEND_SEQTWOBYTESTR(str, len, attrs) \
198 dtrace:helper:ustack: \
199 /!this->done && len > 0 && TWOBYTE_SEQSTR(attrs)/ \
202 this->stbuf = (uint16_t *)alloca(MAX_TWOBYTESTR_BYTES + 2); \
203 copyinto(str + V8_OFF_TWOBYTESTR_CHARS, \
204 MAX_TWOBYTESTR_BYTES, this->stbuf); \
205 this->stbuf[MAX_TWOBYTESTR_BYTES - 1] = '\0'; \
206 this->stbuf[MAX_TWOBYTESTR_BYTES] = '\0'; \
208 LOOP_8(LOOP_8(LOOP_4(LOOP_ITER(this->i, len, \
209 MAX_TWOBYTESTR_CHARS, \
210 APPEND_CHR(TO_ASCII(this->stbuf[this->i])))))) \
217 * Print out the given Node.js ExternalString, or do nothing if the string is
218 * not an ASCII ExternalString.
220 #define APPEND_NODESTR(str, len, attrs) \
221 dtrace:helper:ustack: \
222 /!this->done && len > 0 && ASCII_EXTSTR(attrs)/ \
224 this->resource = COPYIN_PTR(str + V8_OFF_EXTSTR_RSRC); \
225 this->dataptr = COPYIN_PTR(this->resource + NODE_OFF_EXTSTR_DATA); \
226 copyinto(this->dataptr, len, this->buf + this->off); \
231 * Recall that each ConsString points to two other strings which are
232 * semantically concatenated. Of course, these strings may themselves by
233 * ConsStrings, but in D we can only expand this recursion to a finite level.
234 * Thankfully, function and script names are generally not more than a few
235 * levels deep, so we unroll the expansion up to three levels. Even this is
236 * pretty hairy: we use strings "s0", ..., "s13", (each with "str", "len", and
237 * "attr" fields -- see above) to store the expanded strings. We expand the
238 * original string into s0 and s7, then s0 into s1 and s4, etc:
242 * / \ <-- 1st expansion
246 * / \ / \ <-- 2nd expansion
249 * / \ / \ / \ / \ <-- 3rd expansion
250 * s2 s3 s5 s6 s9 s10 s12 s13
252 * Of course, for a given string, any of these expansions may not be needed.
253 * For example, we may expand str and find that s0 is already a SeqString,
254 * while s7 requires further expansion. So when we expand a ConsString, we
255 * zero the length of the string itself, and then at the end we print out
256 * all non-zero-length strings in order (including both internal nodes and
257 * leafs in the tree above) to get the final output.
259 #define EXPAND_START() \
260 dtrace:helper:ustack: \
263 this->s0str = this->s1str = this->s2str = (off_t) 0; \
264 this->s3str = this->s4str = this->s5str = (off_t) 0; \
265 this->s6str = this->s7str = this->s8str = (off_t) 0; \
266 this->s9str = this->s10str = this->s11str = (off_t) 0; \
267 this->s12str = this->s13str = (off_t) 0; \
269 this->s0len = this->s1len = this->s2len = (off_t) 0; \
270 this->s3len = this->s4len = this->s5len = (off_t) 0; \
271 this->s6len = this->s7len = this->s8len = (off_t) 0; \
272 this->s9len = this->s10len = this->s11len = (off_t) 0; \
273 this->s12len = this->s13len = (off_t) 0; \
275 this->s0attrs = this->s1attrs = this->s2attrs = 0; \
276 this->s3attrs = this->s4attrs = this->s5attrs = 0; \
277 this->s6attrs = this->s7attrs = this->s8attrs = 0; \
278 this->s9attrs = this->s10attrs = this->s11attrs = 0; \
279 this->s12attrs = this->s13attrs = 0; \
283 * Expand the ConsString "str" (represented by "str", "len", and "attrs") into
284 * strings "s1" (represented by "s1s", "s1l", and "s1a") and "s2" (represented
285 * by "s2s", "s2l", "s2a"). If "str" is not a ConsString, do nothing.
287 #define EXPAND_STR(str, len, attrs, s1s, s1l, s1a, s2s, s2l, s2a) \
288 dtrace:helper:ustack: \
289 /!this->done && len > 0 && IS_CONSSTR(attrs)/ \
293 s1s = COPYIN_PTR(str + V8_OFF_CONSSTR_CAR); \
294 LOAD_STRFIELDS(s1s, s1l, s1a) \
296 s2s = COPYIN_PTR(str + V8_OFF_CONSSTR_CDR); \
297 LOAD_STRFIELDS(s2s, s2l, s2a) \
301 * Print out a ConsString by expanding it up to three levels and printing out
302 * the resulting SeqStrings.
304 #define APPEND_CONSSTR(str, len, attrs) \
306 EXPAND_STR(str, len, attrs, \
307 this->s0str, this->s0len, this->s0attrs, \
308 this->s7str, this->s7len, this->s7attrs) \
309 EXPAND_STR(this->s0str, this->s0len, this->s0attrs, \
310 this->s1str, this->s1len, this->s1attrs, \
311 this->s4str, this->s4len, this->s4attrs) \
312 EXPAND_STR(this->s1str, this->s1len, this->s1attrs, \
313 this->s2str, this->s2len, this->s2attrs, \
314 this->s3str, this->s3len, this->s3attrs) \
315 EXPAND_STR(this->s4str, this->s4len, this->s4attrs, \
316 this->s5str, this->s5len, this->s5attrs, \
317 this->s6str, this->s6len, this->s6attrs) \
318 EXPAND_STR(this->s7str, this->s7len, this->s7attrs, \
319 this->s8str, this->s8len, this->s8attrs, \
320 this->s11str, this->s11len, this->s11attrs) \
321 EXPAND_STR(this->s8str, this->s8len, this->s8attrs, \
322 this->s9str, this->s9len, this->s9attrs, \
323 this->s10str, this->s10len, this->s10attrs) \
324 EXPAND_STR(this->s11str, this->s11len, this->s11attrs, \
325 this->s12str, this->s12len, this->s12attrs, \
326 this->s13str, this->s13len, this->s13attrs) \
328 APPEND_SEQSTR(str, len, attrs) \
329 APPEND_SEQSTR(this->s0str, this->s0len, this->s0attrs) \
330 APPEND_SEQSTR(this->s1str, this->s1len, this->s1attrs) \
331 APPEND_SEQSTR(this->s2str, this->s2len, this->s2attrs) \
332 APPEND_SEQSTR(this->s3str, this->s3len, this->s3attrs) \
333 APPEND_SEQSTR(this->s4str, this->s4len, this->s4attrs) \
334 APPEND_SEQSTR(this->s5str, this->s5len, this->s5attrs) \
335 APPEND_SEQSTR(this->s6str, this->s6len, this->s6attrs) \
336 APPEND_SEQSTR(this->s7str, this->s7len, this->s7attrs) \
337 APPEND_SEQSTR(this->s8str, this->s8len, this->s8attrs) \
338 APPEND_SEQSTR(this->s9str, this->s9len, this->s9attrs) \
339 APPEND_SEQSTR(this->s10str, this->s10len, this->s10attrs) \
340 APPEND_SEQSTR(this->s11str, this->s11len, this->s11attrs) \
341 APPEND_SEQSTR(this->s12str, this->s12len, this->s12attrs) \
342 APPEND_SEQSTR(this->s13str, this->s13len, this->s13attrs) \
346 * Print out the given SeqString, ConsString, or ExternalString.
347 * APPEND_CONSSTR implicitly handles SeqStrings as the degenerate case of an
348 * expanded ConsString.
350 #define APPEND_V8STR(str, len, attrs) \
351 APPEND_CONSSTR(str, len, attrs) \
352 APPEND_NODESTR(str, len, attrs)
355 * In this first clause we initialize all variables. We must explicitly clear
356 * them because they may contain values left over from previous iterations.
358 dtrace:helper:ustack:
363 /* output/flow control */
364 this->buf = (char*) alloca(128);
369 this->ctx = (off_t) 0;
370 this->marker = (off_t) 0;
371 this->func = (off_t) 0;
372 this->shared = (off_t) 0;
373 this->map = (off_t) 0;
375 this->funcnamestr = (off_t) 0;
376 this->funcnamelen = 0;
377 this->funcnameattrs = 0;
378 this->script = (off_t) 0;
379 this->scriptattrs = 0;
380 this->scriptnamestr = (off_t) 0;
381 this->scriptnamelen = 0;
382 this->scriptnameattrs = 0;
384 this->line_ends = (off_t) 0;
387 /* binary search fields */
388 this->bsearch_min = 0;
389 this->bsearch_max = 0;
394 * Like V8, we first check if we've got an ArgumentsAdaptorFrame. We've got
395 * nothing to add for such frames, so we bail out quickly.
397 dtrace:helper:ustack:
399 this->ctx = COPYIN_PTR(this->fp + V8_OFF_FP_CONTEXT);
402 dtrace:helper:ustack:
403 /IS_SMI(this->ctx) && SMI_VALUE(this->ctx) == V8_FT_ADAPTOR/
406 APPEND_CHR8('<','<',' ','a','d','a','p','t');
407 APPEND_CHR8('o','r',' ','>','>','\0','\0','\0');
412 * Check for other common frame types for which we also have nothing to add.
414 dtrace:helper:ustack:
417 this->marker = COPYIN_PTR(this->fp + V8_OFF_FP_MARKER);
420 dtrace:helper:ustack:
421 /!this->done && IS_SMI(this->marker) &&
422 SMI_VALUE(this->marker) == V8_FT_ENTRY/
425 APPEND_CHR8('<','<',' ','e','n','t','r','y');
426 APPEND_CHR4(' ','>','>','\0');
430 dtrace:helper:ustack:
431 /!this->done && IS_SMI(this->marker) &&
432 SMI_VALUE(this->marker) == V8_FT_ENTRYCONSTRUCT/
435 APPEND_CHR8('<','<',' ','e','n','t','r','y');
436 APPEND_CHR8('_','c','o','n','s','t','r','u');
437 APPEND_CHR4('t',' ','>','>');
442 dtrace:helper:ustack:
443 /!this->done && IS_SMI(this->marker) &&
444 SMI_VALUE(this->marker) == V8_FT_EXIT/
447 APPEND_CHR8('<','<',' ','e','x','i','t',' ');
448 APPEND_CHR4('>','>','\0','\0');
452 dtrace:helper:ustack:
453 /!this->done && IS_SMI(this->marker) &&
454 SMI_VALUE(this->marker) == V8_FT_INTERNAL/
457 APPEND_CHR8('<','<',' ','i','n','t','e','r');
458 APPEND_CHR8('n','a','l',' ','>','>','\0','\0');
462 dtrace:helper:ustack:
463 /!this->done && IS_SMI(this->marker) &&
464 SMI_VALUE(this->marker) == V8_FT_CONSTRUCT/
467 APPEND_CHR8('<','<',' ','c','o','n','s','t');
468 APPEND_CHR8('r','u','c','t','o','r',' ','>');
469 APPEND_CHR4('>','\0','\0','\0');
473 dtrace:helper:ustack:
474 /!this->done && IS_SMI(this->marker) &&
475 SMI_VALUE(this->marker) == V8_FT_STUB/
478 APPEND_CHR8('<','<',' ','s','t','u','b',' ');
479 APPEND_CHR4('>','>','\0','\0');
484 * Now check for internal frames that we can only identify by seeing that
485 * there's a Code object where there would be a JSFunction object for a
488 dtrace:helper:ustack:
491 this->func = COPYIN_PTR(this->fp + V8_OFF_FP_FUNC);
492 this->map = V8_MAP_PTR(COPYIN_PTR(this->func + V8_OFF_HEAPOBJ_MAP));
493 this->attrs = COPYIN_UINT8(this->map + V8_OFF_MAP_ATTRS);
496 dtrace:helper:ustack:
497 /!this->done && this->attrs == V8_IT_CODE/
500 APPEND_CHR8('<','<',' ','i','n','t','e','r');
501 APPEND_CHR8('n','a','l',' ','c','o','d','e');
502 APPEND_CHR4(' ','>','>','\0');
507 * At this point, we're either looking at a JavaScriptFrame or an
508 * OptimizedFrame. For now, we assume JavaScript and start by grabbing the
511 dtrace:helper:ustack:
517 this->shared = COPYIN_PTR(this->func + V8_OFF_FUNC_SHARED);
518 this->funcnamestr = COPYIN_PTR(this->shared + V8_OFF_SHARED_NAME);
519 LOAD_STRFIELDS(this->funcnamestr, this->funcnamelen,
520 this->funcnameattrs);
523 dtrace:helper:ustack:
524 /!this->done && this->funcnamelen == 0/
527 * This is an anonymous function, but if it was invoked as a method of
528 * some object then V8 will have computed an inferred name that we can
529 * include in the stack trace.
531 APPEND_CHR8('(','a','n','o','n',')',' ','a');
535 this->funcnamestr = COPYIN_PTR(this->shared + V8_OFF_SHARED_INFERRED);
536 LOAD_STRFIELDS(this->funcnamestr, this->funcnamelen,
537 this->funcnameattrs);
540 dtrace:helper:ustack:
541 /!this->done && this->funcnamelen == 0/
544 APPEND_CHR4('a','n','o','n');
548 APPEND_V8STR(this->funcnamestr, this->funcnamelen, this->funcnameattrs)
551 * Now look for the name of the script where the function was defined. The
552 * "script" itself may be undefined for special functions like "RegExp".
554 dtrace:helper:ustack:
557 this->script = COPYIN_PTR(this->shared + V8_OFF_SHARED_SCRIPT);
558 this->map = V8_MAP_PTR(COPYIN_PTR(this->script + V8_OFF_HEAPOBJ_MAP));
559 this->scriptattrs = COPYIN_UINT8(this->map + V8_OFF_MAP_ATTRS);
562 dtrace:helper:ustack:
563 /!this->done && !V8_TYPE_SCRIPT(this->scriptattrs)/
571 dtrace:helper:ustack:
574 this->scriptnamestr = COPYIN_PTR(this->script + V8_OFF_SCRIPT_NAME);
575 LOAD_STRFIELDS(this->scriptnamestr, this->scriptnamelen,
576 this->scriptnameattrs);
579 dtrace:helper:ustack:
580 /!this->done && this->scriptnamelen != 0/
582 APPEND_CHR4(' ','a','t',' ');
585 APPEND_V8STR(this->scriptnamestr, this->scriptnamelen, this->scriptnameattrs)
588 * Now look for file position and line number information.
590 dtrace:helper:ustack:
593 this->position = COPYIN_UINT32(this->shared + V8_OFF_SHARED_FUNTOK);
594 this->line_ends = COPYIN_PTR(this->script + V8_OFF_SCRIPT_LENDS);
595 this->map = V8_MAP_PTR(COPYIN_PTR(this->line_ends + V8_OFF_HEAPOBJ_MAP));
596 this->le_attrs = COPYIN_UINT8(this->map + V8_OFF_MAP_ATTRS);
599 dtrace:helper:ustack:
600 /!this->done && this->le_attrs != V8_IT_FIXEDARRAY && this->position == 0/
607 dtrace:helper:ustack:
608 /!this->done && this->le_attrs != V8_IT_FIXEDARRAY/
611 * If the line number array was not a valid FixedArray, it's probably
612 * undefined because V8 has not had to compute it yet. In this case we
613 * just show the raw position and call it a day.
615 APPEND_CHR4(' ','p','o','s');
617 APPEND_NUM(SMI_VALUE(this->position));
624 * At this point, we've got both a position in the script and an array
625 * describing where each line of the file ends. We can use this to compute the
626 * line number by binary searching the array. (This is also what V8 does when
627 * computing stack traces.)
629 dtrace:helper:ustack:
632 /* initialize binary search */
633 this->bsearch_line = this->position <
634 SMI_VALUE(COPYIN_PTR(this->line_ends + V8_OFF_FA_DATA)) ? 1 : 0;
635 this->bsearch_min = 0;
636 this->bsearch_max = this->bsearch_line != 0 ? 0 :
637 SMI_VALUE(COPYIN_PTR(this->line_ends + V8_OFF_FA_SIZE)) - 1;
641 * Of course, we can't iterate the binary search indefinitely, so we hardcode 15
642 * iterations. That's enough to precisely identify the line number in files up
643 * to 32768 lines of code.
645 #define BSEARCH_LOOP \
646 dtrace:helper:ustack: \
647 /!this->done && this->bsearch_max >= 1/ \
649 this->ii = (this->bsearch_min + this->bsearch_max) >> 1; \
652 dtrace:helper:ustack: \
653 /!this->done && this->bsearch_max >= 1 && \
654 this->position > SMI_VALUE( \
655 COPYIN_PTR(this->line_ends + V8_OFF_FA_DATA + \
656 this->ii * sizeof (uint32_t)))/ \
658 this->bsearch_min = this->ii + 1; \
661 dtrace:helper:ustack: \
662 /!this->done && this->bsearch_max >= 1 && \
663 this->position <= SMI_VALUE( \
664 COPYIN_PTR(this->line_ends + V8_OFF_FA_DATA + \
665 (this->ii - 1) * sizeof (uint32_t)))/ \
667 this->bsearch_max = this->ii - 1; \
686 dtrace:helper:ustack:
687 /!this->done && !this->bsearch_line/
689 this->bsearch_line = this->ii + 1;
692 dtrace:helper:ustack:
696 APPEND_CHR4('l','i','n','e');
698 APPEND_NUM(this->bsearch_line);
704 /* vim: set tabstop=8 softtabstop=8 shiftwidth=8 noexpandtab: */