From 6ae1a37fd33b50c519a78e23465d844c22fd4030 Mon Sep 17 00:00:00 2001 From: Alok Kumar Sharma Date: Sun, 24 Jul 2022 15:44:04 +0530 Subject: [PATCH] Enable Access to containing scope variables from nested inlined function Currently inlined functions are barred from accessing any containing local scope variable, which makes sense for global scoped functions. The gcc compiler supports nested (local scoped) function for end user, the Clang compiler does not allow nested function for end user but compiler itself generates such artificial functions internally ( in case of OpenMP). General barrier by gdb reduces debug experience, current patch enhances debugging in such cases. Please consider below program --------------------- 1 #include 2 3 __attribute__((always_inline)) inline int outer_fun (int arg) { 4 // Inlined at (A) or not, this function should not have access to 5 // variables caller_fun_var, caller_fun_other_var in non containing 6 // scoped function caller_fun. 7 int outer_local = 5; 8 outer_local = arg; 9 printf("Value of outer_local = %d\n", outer_local); 10 } 11 12 void caller_fun() { 13 static int caller_fun_var = 9; 14 volatile int caller_fun_other_var = 7; 15 __attribute__((always_inline)) inline void called_fun (int arg) { 16 // Inlined at (B) or not, this function should have access to 17 // containing scope variables caller_fun_var, caller_fun_other_var 18 int local = 0; 19 local = arg; 20 printf("Value of local = %d\n", local); 21 } 22 23 outer_fun(caller_fun_other_var); // <--- (A) 24 called_fun(caller_fun_other_var); // <--- (B) 25 return; 26 } 27 28 int main() { 29 caller_fun(); 30 return 0; 31 } ----------------------- Which produces DWARF as below ----------------------- 0x00000094: DW_TAG_subprogram DW_AT_name ("main") 0x000000b2: DW_TAG_subprogram DW_AT_name ("caller_fun") 0x000000d0: DW_TAG_variable DW_AT_name ("caller_fun_var") 0x000000dd: DW_TAG_variable DW_AT_name ("caller_fun_other_var") 0x000000ec: DW_TAG_subprogram DW_AT_name ("called_fun") 0x00000112: DW_TAG_inlined_subroutine DW_AT_abstract_origin (0x00000169 "outer_fun") 0x0000013f: DW_TAG_inlined_subroutine DW_AT_abstract_origin (0x000000ec "called_fun") 0x00000169: DW_TAG_subprogram DW_AT_external (true) DW_AT_name ("outer_fun") ----------------------- Please note concrete instances of abstract functions "called_fun" (0x000000ec) and "outer_fun" (0x00000169) are DIE:0x00000112 and DIE:0x0000013f. Both have DWARF tag DW_TAG_inlined_subroutine. To stop "outer_fun" (0x00000112) from accessing scope of "caller_fun", gdb generalize barrier for DW_TAG_inlined_subroutine. GDB should instead check DW_AT_abstract_origin attribute to check actual scope of corresponding abstract functions and allow "called_fun" to access scope of "caller_fun". Current patch stores this information into data structure DIE_BLOCK and uses that to compute actual containing scope. Change-Id: I7b9179157fc0513b39908e39c9fbc740ac1a8943 --- gdb/buildsym.c | 148 +++++++++++++++++ gdb/buildsym.h | 25 +++ gdb/dwarf2/read.c | 43 ++++- gdb/symtab.c | 12 +- gdb/symtab.h | 14 ++ gdb/testsuite/gdb.base/nested_inlined_fun.c | 92 +++++++++++ gdb/testsuite/gdb.base/nested_inlined_fun.exp | 154 ++++++++++++++++++ gdb/testsuite/gdb.threads/omp-task.c | 18 ++ gdb/testsuite/gdb.threads/omp-task.exp | 8 +- 9 files changed, 508 insertions(+), 6 deletions(-) create mode 100644 gdb/testsuite/gdb.base/nested_inlined_fun.c create mode 100644 gdb/testsuite/gdb.base/nested_inlined_fun.exp diff --git a/gdb/buildsym.c b/gdb/buildsym.c index 65c2ac5aff0..058c005fd02 100644 --- a/gdb/buildsym.c +++ b/gdb/buildsym.c @@ -31,6 +31,8 @@ #include "macrotab.h" #include "demangle.h" /* Needed by SYMBOL_INIT_DEMANGLED_NAME. */ #include "block.h" +#include "dwarf2/attribute.h" +#include "dwarf2/die.h" #include "cp-support.h" #include "dictionary.h" #include @@ -49,6 +51,25 @@ struct pending_block struct block *block; }; +/* Data structure to contain DIE, generated BLOCK for it and in case + DIE is inlined its ORIG_DIE which is DIE represented by the + DW_AT_abstract_origin attribute. */ + +struct die_block + { + struct die_info *die; + struct die_info *orig_die; + struct block *block; + }; + +/* List of DIE_BLOCKs */ + +struct die_block_list + { + struct die_block_list *next; + struct die_block mem; + }; + buildsym_compunit::buildsym_compunit (struct objfile *objfile_, const char *name, const char *comp_dir_, @@ -191,6 +212,26 @@ buildsym_compunit::record_pending_block (struct block *block, } } +/* Record DIE_BLOCK (DIE, ORIG_DIE, BLOCK) for all the blocks generated + and insert it at the start in the list m_die_block_list. */ + +void +buildsym_compunit::record_die_block (struct die_info *die, + struct die_info *orig_die, + struct block *block) +{ + struct die_block_list* pdie_block; + + pdie_block = XOBNEW (&m_die_block_obstack, struct die_block_list); + + pdie_block->mem.die = die; + pdie_block->mem.orig_die = orig_die; + pdie_block->mem.block = block; + pdie_block->next = m_die_block_list; + + m_die_block_list = pdie_block; +} + /* Take one of the lists of symbols and make a block from it. Keep the order the symbols have in the list (reversed from the input file). Put the block on the list of pending blocks. */ @@ -483,6 +524,109 @@ buildsym_compunit::make_blockvector () return (blockvector); } + +/* Compare two DIE_BLOCKs. This is used to sort DIE_BLOCK array. */ + +static int +compare_die (const void *lp, const void *rp) +{ + struct die_block *left = (struct die_block *) lp; + struct die_block *right = (struct die_block *) rp; + + return (int) left->die->sect_off - (int) right->die->sect_off; +} + + +/* This function sets m_scopeblock field of SYMBOL's for INLINED functions. */ + +void +buildsym_compunit::finish_inlined_scope () +{ + struct die_block_list *next; + struct die_block *die_block_arr = nullptr; + struct die_block key, *parent_die_block, *scope_die_block; + struct symbol *sym; + struct block *block1; + int i = 0, count = 0; + + /* Count number of elements in DIE_BLOCK_LIST m_die_block_list. */ + + for (next = m_die_block_list, i = 0; next; next = next->next, i++) + { + } + + count = i; + + /* Copy elements to the local array die_block_arr. */ + + die_block_arr = XOBNEWVEC (&m_die_block_obstack, struct die_block, count); + + for (next = m_die_block_list, i = 0; next; next = next->next, i++) + { + die_block_arr[i] = next->mem; + } + + free_die_block (); + + /* Sort array die_block_arr for better performance. */ + + qsort (die_block_arr, count, sizeof(struct die_block), compare_die); + + /* Let's iterate over all the die-block pairs. And find its original + lexical scope. */ + for (i = 0; i < count; i++) + { + block1 = die_block_arr[i].block; + sym = block1->function(); + + /* Skip this if, this block is not for function, or + this block is not inlined. */ + if (!sym || !die_block_arr[i].orig_die) + continue; + + /* Let's find whether current die's parent die has generated block.*/ + key.die = die_block_arr[i].die->parent; + parent_die_block = (struct die_block*) bsearch (&key, die_block_arr, + count, sizeof (struct die_block), + compare_die); + + if (parent_die_block && parent_die_block->orig_die) + { + /* If current die and its parent both have orign die's (both are + inlined and those origin (abstract functions) are also having same + child parent relationship. This is the case when caller inlined + function is also lexically containing called function. */ + if (die_block_arr[i].orig_die->parent == parent_die_block->orig_die) + sym->set_scopeblock ((struct block *) block1->superblock()); + } + else + { + /* Abstract functions usually do not generate block, but let us not + assume that and start searching from the DIE and let's go through + the parent chain, trying to find a DIE which results in a block + being created. */ + struct die_info *scope_die = die_block_arr[i].orig_die; + scope_die_block = nullptr; + while (scope_die) + { + key.die = scope_die; + scope_die_block = (struct die_block*) bsearch (&key, + die_block_arr, + count, sizeof (struct die_block), compare_die); + if (scope_die_block) + break; + scope_die = scope_die->parent; + } + + if (scope_die_block) + sym->set_scopeblock ((struct block *) scope_die_block->block); + } + } + + die_block_arr = nullptr; + m_die_block_obstack.clear (); +} + /* Start recording information about source code that came from an included (or otherwise merged-in) source file with a different @@ -882,6 +1026,10 @@ buildsym_compunit::end_compunit_symtab_with_blockvector 1, expandable); blockvector = make_blockvector (); + /* Find and set the search scope of concrete inlined instances to the + corresponding abstract instance. */ + finish_inlined_scope(); + /* Read the line table if it has to be read separately. This is only used by xcoffread.c. */ if (m_objfile->sf->sym_read_linetable != NULL) diff --git a/gdb/buildsym.h b/gdb/buildsym.h index 2ad00336269..6cbb33fa6c6 100644 --- a/gdb/buildsym.h +++ b/gdb/buildsym.h @@ -190,6 +190,20 @@ struct buildsym_compunit m_pending_blocks = nullptr; } + /* This function is called to discard any die_block. */ + + void free_die_block () + { + m_die_block_obstack.clear (); + m_die_block_list = nullptr; + } + + /* This function inserts a new element to the list m_die_block_list. */ + + void record_die_block (struct die_info *die, + struct die_info *orig_die, + struct block *block); + struct block *finish_block (struct symbol *symbol, struct pending_block *old_blocks, const struct dynamic_prop *static_link, @@ -316,6 +330,8 @@ struct buildsym_compunit struct blockvector *make_blockvector (); + void finish_inlined_scope (); + void watch_main_source_file_lossage (); struct compunit_symtab *end_compunit_symtab_with_blockvector @@ -397,12 +413,21 @@ struct buildsym_compunit /* An obstack used for allocating pending blocks. */ auto_obstack m_pending_block_obstack; + /* An obstack used for allocating die_blocks. */ + auto_obstack m_die_block_obstack; + /* Pointer to the head of a linked list of symbol blocks which have already been finalized (lexical contexts already closed) and which are just waiting to be built into a blockvector when finalizing the associated symtab. */ struct pending_block *m_pending_blocks = nullptr; + /* Pointer to DIE_BLOCKs which is group of + * DIE + * original die ORIG_DIE (abstract instance) + * BLOCK generated for it. */ + struct die_block_list *m_die_block_list = nullptr; + /* Pending static symbols and types at the top level. */ struct pending *m_file_symbols = nullptr; diff --git a/gdb/dwarf2/read.c b/gdb/dwarf2/read.c index f7e55af3e86..af40b8deb1c 100644 --- a/gdb/dwarf2/read.c +++ b/gdb/dwarf2/read.c @@ -11925,6 +11925,41 @@ inherit_abstract_dies (struct die_info *die, struct dwarf2_cu *cu) compute_delayed_physnames (origin_cu); } +static struct block* +finish_die_block (struct dwarf2_cu *cu, + struct die_info *die, + struct symbol *symbol, + struct pending_block *old_blocks, + const struct dynamic_prop *static_link, + CORE_ADDR start, CORE_ADDR end) +{ + struct block *block; + struct attribute *orig_attr; + struct die_info *orig_die; + + block = cu->get_builder ()->finish_block (symbol, old_blocks, + static_link, start, end); + if (block) + { + orig_attr = dwarf2_attr (die, DW_AT_abstract_origin, cu); + orig_die = nullptr; + + if (orig_attr + && ((block->function () != NULL && block_inlined_p (block)) + || die->tag == DW_TAG_lexical_block)) + { + struct dwarf2_cu *origin_cu = cu; + orig_die = follow_die_ref (die, + orig_attr, + &origin_cu); + } + + cu->get_builder ()->record_die_block (die, orig_die, block); + } + + return block; +} + static void read_func_scope (struct die_info *die, struct dwarf2_cu *cu) { @@ -12075,8 +12110,8 @@ read_func_scope (struct die_info *die, struct dwarf2_cu *cu) struct context_stack cstk = cu->get_builder ()->pop_context (); /* Make a block for the local symbols within. */ - block = cu->get_builder ()->finish_block (cstk.name, cstk.old_blocks, - cstk.static_link, lowpc, highpc); + block = finish_die_block (cu, die, cstk.name, cstk.old_blocks, + cstk.static_link, lowpc, highpc); /* For C++, set the block's scope. */ if ((cu->lang () == language_cplus @@ -12188,8 +12223,8 @@ read_lexical_block_scope (struct die_info *die, struct dwarf2_cu *cu) || (*cu->get_builder ()->get_local_using_directives ()) != NULL) { struct block *block - = cu->get_builder ()->finish_block (0, cstk.old_blocks, NULL, - cstk.start_addr, highpc); + = finish_die_block (cu, die, 0, cstk.old_blocks, NULL, + cstk.start_addr, highpc); /* Note that recording ranges after traversing children, as we do here, means that recording a parent's ranges entails diff --git a/gdb/symtab.c b/gdb/symtab.c index 03aa2a96b87..87aa59538c8 100644 --- a/gdb/symtab.c +++ b/gdb/symtab.c @@ -2228,7 +2228,17 @@ lookup_local_symbol (const char *name, } if (block->function () != NULL && block_inlined_p (block)) - break; + { + struct symbol *func = block->function(); + struct block *scopeblock = func->scopeblock(); + /* For inlines, check in the scope of the abstract instance. */ + if (scopeblock && scopeblock != block_global_block (block)) + { + block = scopeblock; + continue; + } + break; + } block = block->superblock (); } diff --git a/gdb/symtab.h b/gdb/symtab.h index ac902a4cbbe..0f765e2aec3 100644 --- a/gdb/symtab.h +++ b/gdb/symtab.h @@ -1282,6 +1282,16 @@ struct symbol : public general_symbol_info, public allocate_on_obstack return this->subclass == SYMBOL_TEMPLATE; } + struct block *scopeblock () const + { + return m_scopeblock; + } + + void set_scopeblock (struct block *scopeblock) + { + m_scopeblock = scopeblock; + } + struct type *type () const { return m_type; @@ -1399,6 +1409,10 @@ struct symbol : public general_symbol_info, public allocate_on_obstack void set_symtab (struct symtab *symtab); + /* Original scope (before inlining) block of inlined function. */ + + struct block *m_scopeblock; + /* Data type of value */ struct type *m_type = nullptr; diff --git a/gdb/testsuite/gdb.base/nested_inlined_fun.c b/gdb/testsuite/gdb.base/nested_inlined_fun.c new file mode 100644 index 00000000000..6012027fbc9 --- /dev/null +++ b/gdb/testsuite/gdb.base/nested_inlined_fun.c @@ -0,0 +1,92 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2022 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Tests which verify that GDB can access scope variables for nested functions + properly when inlined. +*/ + +#include + +static int static_var = 4; + +__attribute__((always_inline)) inline int outer_fun (int arg) { + int outer_local = 5; + outer_local = arg; + printf("Value of outer_local = %d\n", outer_local); +} + +void caller_fun() { + static int caller_fun_var = 9; + volatile int caller_fun_other_var = 7; + __attribute__((always_inline)) inline void inlined_called_fun (int arg) { + int local = 0; + local = arg; + caller_fun_var += arg; + printf("Value of local = %d\n", local); + } + + inlined_called_fun(caller_fun_other_var); + + outer_fun(caller_fun_other_var); + + inlined_called_fun(caller_fun_other_var); + + { + volatile int lex_local = 11; + __attribute__((always_inline)) inline void lex_called_fun (int arg) { + int local = 0; + local = arg; /* break here for lex_called_fun*/ + printf("Value of local = %d\n", local); + } + lex_called_fun(lex_local+5); + } + + return; +} + +__attribute__((always_inline)) inline static void inlined_caller_fun() { + static int i_caller_fun_var = 9; + volatile int i_caller_fun_other_var = 7; + __attribute__((always_inline)) inline void inlined_called_fun (int arg) { + int local = 0; + local = arg; + i_caller_fun_var += arg; + printf("Value of local = %d\n", local); + } + + inlined_called_fun(i_caller_fun_other_var); + + outer_fun(i_caller_fun_other_var); + + { + volatile int i_lex_local = 11; + __attribute__((always_inline)) inline void i_lex_called_fun (int arg) { + int local = 0; + local = arg; /* break here for i_lex_called_fun*/ + printf("Value of local = %d\n", local); + } + i_lex_called_fun(i_lex_local+5); + } + + return; +} + +int main() { + caller_fun(); + inlined_caller_fun(); + return 0; +} diff --git a/gdb/testsuite/gdb.base/nested_inlined_fun.exp b/gdb/testsuite/gdb.base/nested_inlined_fun.exp new file mode 100644 index 00000000000..14a67ace0da --- /dev/null +++ b/gdb/testsuite/gdb.base/nested_inlined_fun.exp @@ -0,0 +1,154 @@ +# Copyright 2022 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This file is part of the gdb testsuite. + +# Tests which verify that GDB can access scope variables for nested functions +# properly when inlined. + +standard_testfile + +if {![support_nested_function_tests]} { + return -1 +} + +if {[prepare_for_testing "failed to prepare" $testfile $srcfile]} { + return -1 +} + +if {![runto_main]} { + return -1 +} + +gdb_test "break caller_fun" \ + ".*" \ + "break caller_fun" +gdb_test "continue" \ + ".*Breakpoint 2.*" \ + "continue 1" +gdb_test "break inlined_called_fun" \ + ".*" \ + "break inlined_called_fun" +gdb_test "break outer_fun" \ + ".*" \ + "break outer_fun" + +# Containing scope variables should be accessible +# from inlined_called_fun (first call). + +gdb_test "continue" \ + ".*Breakpoint 3.*" \ + "continue 2" +gdb_test "print caller_fun_var" \ + "= 9" \ + "print caller_fun_var 1" +gdb_test "print caller_fun_other_var" \ + "= 7" \ + "print caller_fun_other_var 1" + +# Containing scope variables should not be accessible from outer_fun. + +gdb_test "continue" \ + ".*Breakpoint 4.*" \ + "continue 3" +gdb_test "print caller_fun_var" \ + "No symbol \"caller_fun_var\" in current context." \ + "print caller_fun_var 2" +gdb_test "print caller_fun_other_var" \ + "No symbol \"caller_fun_other_var\" in current context." \ + "print caller_fun_other_var 2" + +# Containing scope variables should be accessible +# from inlined_called_fun (second call). + +gdb_test "continue" \ + ".*Breakpoint 3.*" \ + "continue 4" +gdb_test "print caller_fun_var" \ + "= 16" \ + "print caller_fun_var 3" +gdb_test "print caller_fun_other_var" \ + "= 7" \ + "print caller_fun_other_var 3" + +# Inlined function 'lex_called_fun' inside lexical block can access +# containing scope (containing function and lexical block). + +gdb_breakpoint [gdb_get_line_number "break here for lex_called_fun"] +gdb_test "continue" \ + ".*Breakpoint 5.*" \ + "continue 5" +gdb_test "print caller_fun_var" \ + "= 23" \ + "print caller_fun_var 4" +gdb_test "print caller_fun_other_var" \ + "= 7" \ + "print caller_fun_other_var 4" +gdb_test "print lex_local" \ + "= 11" \ + "print lex_local" + +# Testing inlined caller function. + +gdb_test "break inlined_caller_fun" \ + ".*" \ + "break inlined_caller_fun" +gdb_test "continue" \ + ".*Breakpoint 6.*" \ + "continue 6" +gdb_test "break inlined_called_fun" \ + ".*" \ + "break inlined_ccalled_fun" + +# Inlined function inside inlined function can access containing scope. + +gdb_test "continue" \ + ".*Breakpoint 3.*" \ + "continue 7" +gdb_test "print i_caller_fun_var" \ + "= 9" \ + "print i_caller_fun_var 1" +gdb_test "print i_caller_fun_other_var" \ + "= 7" \ + "print i_caller_fun_other_var 1" + +# Containing scope variables should not be accessible from outer_fun. + +gdb_test "continue" \ + ".*Breakpoint 4.*" \ + "continue 8" +gdb_test "print i_caller_fun_var" \ + "No symbol \"i_caller_fun_var\" in current context." \ + "print i_caller_fun_var 2" +gdb_test "print i_caller_fun_other_var" \ + "No symbol \"i_caller_fun_other_var\" in current context." \ + "print i_caller_fun_other_var 2" + +# Inlined function 'i_lex_called_fun' inside lexical block can access +# containing scope (containing function and lexical block). + +gdb_breakpoint [gdb_get_line_number "break here for i_lex_called_fun"] +gdb_test "continue" \ + ".*Breakpoint 8.*" \ + "continue 9" +gdb_test "print i_caller_fun_var" \ + "= 16" \ + "print i_caller_fun_var 3" +gdb_test "print i_caller_fun_other_var" \ + "= 7" \ + "print i_caller_fun_other_var 3" +gdb_test "print i_lex_local" \ + "= 11" \ + "print i_lex_local" diff --git a/gdb/testsuite/gdb.threads/omp-task.c b/gdb/testsuite/gdb.threads/omp-task.c index bdeeae501cb..4242e0e6e42 100644 --- a/gdb/testsuite/gdb.threads/omp-task.c +++ b/gdb/testsuite/gdb.threads/omp-task.c @@ -21,20 +21,38 @@ #include #include +#include + +int global_var1; +int global_var2 = 99; int foo(int n) { int share1 = 9, share2 = 11, share3 = 13, priv1, priv2, fpriv; + int same_var = 5; + int other_var = 21; + static int static_var = 93; + global_var1 = 99; fpriv = n + 4; if (n < 2) return n; else { + int same_var = rand() % 5; + int local_var = 31; #pragma omp task shared(share1, share2) private(priv1, priv2) firstprivate(fpriv) shared(share3) { + volatile int lex_local = 77; priv1 = n; priv2 = n + 2; share2 += share3; printf("share1 = %d, share2 = %d, share3 = %d\n", share1, share2, share3); + printf("global_var1 = %d\n", global_var1); + printf("global_var2 = %d\n", global_var2); + printf("static_var = %d\n", static_var); + printf("same_var = %d\n", same_var); + printf("other_var = %d\n", other_var); + printf("local_var = %d\n", local_var); + printf("lex_local = %d\n", lex_local); share1 = priv1 + priv2 + fpriv + foo(n - 1) + share2 + share3; } #pragma omp taskwait diff --git a/gdb/testsuite/gdb.threads/omp-task.exp b/gdb/testsuite/gdb.threads/omp-task.exp index 8f46658fe0d..e79a5ba5ba6 100644 --- a/gdb/testsuite/gdb.threads/omp-task.exp +++ b/gdb/testsuite/gdb.threads/omp-task.exp @@ -20,7 +20,6 @@ standard_testfile -set have_nested_function_support 0 set opts {openmp debug} if {[prepare_for_testing "failed to prepare" $testfile $srcfile $opts]} { @@ -49,3 +48,10 @@ gdb_test "continue" ".*Breakpoint 3.*" "continue 2" gdb_test "print priv1" "= 10" gdb_test "print priv2" "= 12" gdb_test "print fpriv" "= 14" +gdb_test "print global_var1" "= 99" +gdb_test "print global_var2" "= 99" +gdb_test "print static_var" "= 93" +gdb_test "print same_var" "= .*" +gdb_test "print other_var" "= 21" +gdb_test "print local_var" "= 31" +gdb_test "print lex_local" "= 77" -- 2.25.1