Repository: pwndbg/pwndbg

Issue:
`nearpc` fails to show the symbol name after demangling
When I learning OOP for C++, I found out this:

```console
$ cat test.cpp
#include <iostream>

class A{
public:
    A(int _sz=10);
    A(const A& a);
    ~A();
    A& operator=(const A& a);
private:
    char *p;
    int sz;
};

A::A(int _sz): sz(_sz){
    p = new char[sz];
    std::cout << "ctor" << std::endl;
}

A::A(const A &a) {
    p = new char[sz=a.sz];
    std::cout << "copy" << std::endl;
}

A::~A() {
    delete[] p;
    std::cout << "dtor" << std::endl;
}

A &A::operator=(const A &a) {
    if(this != &a){
        p = new char[sz=a.sz];
        for(int i = 0; i < sz; ++i) p[i] = a.p[i];
    }
    std::cout << "assign" << std::endl;
    return *this;
}

int main() {
    A a1;
    A a2 = a1;
    A a3;
    a3 = a2;
    return 0;
}
$ g++ -ggdb test.cpp
$ gdb -q a.out -ex 'set context-sections' -ex 'break main' -ex 'run' -ex 'nearpc main 15'
...
 ► 0x55f188f603fe <main>        endbr64
   0x55f188f60402 <main+4>      push   rbp
   0x55f188f60403 <main+5>      mov    rbp, rsp
   0x55f188f60406 <main+8>      push   rbx
   0x55f188f60407 <main+9>      sub    rsp, 0x48
   0x55f188f6040b <main+13>     mov    rax, qword ptr fs:[0x28]      <main>
   0x55f188f60414 <main+22>     mov    qword ptr [rbp - 0x18], rax
   0x55f188f60418 <main+26>     xor    eax, eax
   0x55f188f6041a <main+28>     lea    rax, [rbp - 0x50]
   0x55f188f6041e <main+32>     mov    esi, 0xa
   0x55f188f60423 <main+37>     mov    rdi, rax
   0x55f188f60426 <main+40>     call   A::A(int)                <A::A(int)>

   0x55f188f6042b <main+45>     lea    rdx, [rbp - 0x50]
   0x55f188f6042f <main+49>     lea    rax, [rbp - 0x40]
   0x55f188f60433 <main+53>     mov    rsi, rdx
   0x55f188f60436 <main+56>     mov    rdi, rax
   0x55f188f60439 <main+59>     call   0x55f188f6028e                <0x55f188f6028e>

   0x55f188f6043e <main+64>     lea    rax, [rbp - 0x30]
   0x55f188f60442 <main+68>     mov    esi, 0xa
   0x55f188f60447 <main+73>     mov    rdi, rax
   0x55f188f6044a <main+76>     call   A::A(int)                <A::A(int)>

   0x55f188f6044f <main+81>     lea    rdx, [rbp - 0x40]
   0x55f188f60453 <main+85>     lea    rax, [rbp - 0x30]
   0x55f188f60457 <main+89>     mov    rsi, rdx
   0x55f188f6045a <main+92>     mov    rdi, rax
   0x55f188f6045d <main+95>     call   0x55f188f6034c                <0x55f188f6034c>

   0x55f188f60462 <main+100>    mov    ebx, 0
   0x55f188f60467 <main+105>    lea    rax, [rbp - 0x30]
   0x55f188f6046b <main+109>    mov    rdi, rax
   0x55f188f6046e <main+112>    call   A::~A()                <A::~A()>

   0x55f188f60473 <main+117>    lea    rax, [rbp - 0x40]
...
```

As you can see, `nearpc` failed to show the symbol name of the copy constructor(at 0x55f188f6028e) and the assignment operator(at 0x55f188f6034c).

It's because the symbol of them after demangling contains spaces and break our logic at here:

https://github.com/pwndbg/pwndbg/blob/e642461941b47fba5f68ada70cb489ede0fa5457/pwndbg/gdblib/symbol.py#L92-L99



Discussion:
Shorter example - compile as `g++ -O0 mangling.cpp`:
```cpp
struct A {
    void foo(int, int) { };
};

int main() {
    A a;
    a.foo(1, 1);
}
```
Heh, the output of `info symbol ...` is not so nice: https://github.com/bminor/binutils-gdb/blob/d1702fea87aa62dff7de465464097dba63cc8c0f/gdb/printcmd.c#L1594-L1624
To give another example: for the symbol above GDB's `info symbol ...` returns:
```
'A::foo(int, int) [clone.isra.0] + 3 in section .text of /root/pwndbg/tests/gdb-tests/tests/binaries/a.out\n'
```

And since we split this by 3 spaces we end up with the following split:
```
['A::foo(int,',
 'int)',
 '[clone.isra.0] + 3 in section .text of /root/pwndbg/tests/gdb-tests/tests/binaries/a.out\n']
 ```

And we end up returning an empty string here:
```py
    # Expected format looks like this:
    # main in section .text of /bin/bash
    # main + 3 in section .text of /bin/bash
    # system + 1 in section .text of /lib/x86_64-linux-gnu/libc.so.6
    # No symbol matches system-1.
    a, b, c, _ = result.split(maxsplit=3)

    if b == "+":
        return "%s+%s" % (a, c)
    if b == "in":
        return a

    return ""
```


Resolving patch:
diff --git a/pwndbg/gdblib/symbol.py b/pwndbg/gdblib/symbol.py
--- a/pwndbg/gdblib/symbol.py
+++ b/pwndbg/gdblib/symbol.py
@@ -84,19 +84,25 @@ def get(address: int, gdb_only=False) -> str:
                 res = pwndbg.ida.Name(address) or pwndbg.ida.GetFuncOffset(address)
                 return res or ""
 
-    # Expected format looks like this:
-    # main in section .text of /bin/bash
-    # main + 3 in section .text of /bin/bash
-    # system + 1 in section .text of /lib/x86_64-linux-gnu/libc.so.6
-    # No symbol matches system-1.
-    a, b, c, _ = result.split(maxsplit=3)
-
-    if b == "+":
-        return "%s+%s" % (a, c)
-    if b == "in":
-        return a
+    # See https://github.com/bminor/binutils-gdb/blob/d1702fea87aa62dff7de465464097dba63cc8c0f/gdb/printcmd.c#L1594-L1624
+    # The most often encountered formats looks like this:
+    #   "main in section .text of /bin/bash"
+    #   "main + 3 in section .text of /bin/bash"
+    #   "system + 1 in section .text of /lib/x86_64-linux-gnu/libc.so.6"
+    #   "No symbol matches system-1"
+    # But there are some others that we have to account for as well
+    if " in section " in result:
+        loc_string, _ = result.split(" in section ")
+    elif " in load address range of " in result:
+        loc_string, _ = result.split(" in load address range of ")
+    elif " overlay section " in result:
+        result, _ = result.split(" overlay section ")
+        loc_string, _ = result.split(" in ")
+    else:
+        loc_string = ""
 
-    return ""
+    # If there is 'main + 87' we want to replace it with 'main+87' etc.
+    return loc_string.replace(" + ", "+")
 
 
 @pwndbg.lib.memoize.reset_on_objfile


Test patch:
diff --git a/tests/gdb-tests/tests/binaries/makefile b/tests/gdb-tests/tests/binaries/makefile
--- a/tests/gdb-tests/tests/binaries/makefile
+++ b/tests/gdb-tests/tests/binaries/makefile
@@ -38,7 +38,7 @@ GLIBC_2_33=$(PWD)/glibcs/2.33
 
 .PHONY : all clean
 
-CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out
+CUSTOM_TARGETS = reference_bin_pie.out reference_bin_nopie.out symbol_1600_and_752.out
 
 all: $(LINKED) $(LINKED_ASM) $(COMPILED_GO) $(CUSTOM_TARGETS)
 
@@ -113,7 +113,7 @@ issue_1565.out: issue_1565.c
 
 clean :
 	@echo "[+] Cleaning stuff"
-	@rm -f $(COMPILED) $(LINKED) $(COMPILED_ASM) $(LINKED_ASM) $(COMPILED_GO)
+	@rm -f $(COMPILED) $(LINKED) $(COMPILED_ASM) $(LINKED_ASM) $(COMPILED_GO) *.out *.o
 
 
 reference_bin_pie.out: reference-binary.c
@@ -123,3 +123,6 @@ reference_bin_pie.out: reference-binary.c
 reference_bin_nopie.out: reference-binary.c
 	@echo "[+] Building reference_bin_nopie.out"
 	${ZIGCC} -fno-pie -o reference_bin_nopie.out reference-binary.c
+
+symbol_1600_and_752.out: symbol_1600_and_752.cpp
+	${CXX} -O0 -ggdb -Wno-pmf-conversions symbol_1600_and_752.cpp -o symbol_1600_and_752.out
diff --git a/tests/gdb-tests/tests/binaries/symbol_1600_and_752.cpp b/tests/gdb-tests/tests/binaries/symbol_1600_and_752.cpp
new file mode 100644
--- /dev/null
+++ b/tests/gdb-tests/tests/binaries/symbol_1600_and_752.cpp
@@ -0,0 +1,23 @@
+void break_here(void* p) { }
+
+struct A {
+    __attribute__((noinline))
+    void foo(int, int) { break_here(0); }
+
+    void call_foo() { foo(1, 2); }
+};
+
+int main() {
+    break_here((void*)main);
+    break_here((void*)break_here);
+
+    // code for issue 1600
+    break_here((void*)&A::foo);
+
+    // just another check for mangled symbols
+    break_here((void*)&A::call_foo);
+
+    // code for issue 752
+    A a;
+    a.call_foo();
+}
diff --git a/tests/gdb-tests/tests/test_context_commands.py b/tests/gdb-tests/tests/test_context_commands.py
--- a/tests/gdb-tests/tests/test_context_commands.py
+++ b/tests/gdb-tests/tests/test_context_commands.py
@@ -9,6 +9,7 @@
 USE_FDS_BINARY = tests.binaries.get("use-fds.out")
 TABSTOP_BINARY = tests.binaries.get("tabstop.out")
 SYSCALLS_BINARY = tests.binaries.get("syscalls-x64.out")
+MANGLING_BINARY = tests.binaries.get("symbol_1600_and_752.out")
 
 
 def test_context_disasm_show_fd_filepath(start_binary):
@@ -185,3 +186,39 @@ def test_context_disasm_syscalls_args_display(start_binary):
         "   0x4000a5                add    byte ptr [rax], al\n"
         "────────────────────────────────────────────────────────────────────────────────\n"
     )
+
+
+def test_context_backtrace_show_proper_symbol_names(start_binary):
+    start_binary(MANGLING_BINARY)
+    gdb.execute("break A::foo")
+    gdb.execute("continue")
+
+    backtrace = gdb.execute("context backtrace", to_string=True).split("\n")
+
+    assert backtrace[0] == "LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA"
+    assert (
+        backtrace[1]
+        == "─────────────────────────────────[ BACKTRACE ]──────────────────────────────────"
+    )
+
+    assert re.match(r" ► f 0   0x[0-9a-f]+ A::foo\(int, int\)", backtrace[2])
+
+    # Match A::call_foo()+38 or similar: the offset may change so we match \d+ at the end
+    assert re.match(r"   f 1   0x[0-9a-f]+ A::call_foo\(\)\+\d+", backtrace[3])
+
+    # Match main+87 or similar offset
+    assert re.match(r"   f 2   0x[0-9a-f]+ main\+\d+", backtrace[4])
+
+    # Match __libc_start_main+243 or similar offset
+    # Note: on Ubuntu 22.04 there will be __libc_start_call_main and then __libc_start_main
+    # but on older distros there will be only __libc_start_main
+    # Let's not bother too much about it and make it the last call assertion here
+    assert re.match(
+        r"   f 3   0x[0-9a-f]+ (__libc_start_main|__libc_start_call_main)\+\d+", backtrace[5]
+    )
+
+    assert (
+        backtrace[-2]
+        == "────────────────────────────────────────────────────────────────────────────────"
+    )
+    assert backtrace[-1] == ""
diff --git a/tests/gdb-tests/tests/test_symbol.py b/tests/gdb-tests/tests/test_symbol.py
new file mode 100644
--- /dev/null
+++ b/tests/gdb-tests/tests/test_symbol.py
@@ -0,0 +1,30 @@
+import gdb
+
+import pwndbg
+import tests
+
+MANGLING_BINARY = tests.binaries.get("symbol_1600_and_752.out")
+
+
+def test_symbol_get(start_binary):
+    start_binary(MANGLING_BINARY)
+    gdb.execute("break break_here")
+
+    def get_next_ptr():
+        gdb.execute("continue")
+
+        # To fetch the value of 'p' it must be set first
+        # and it will be set by the program copying from register to the stack
+        # (we pass `to_string=True` to suppress the context output)
+        gdb.execute("nextret", to_string=True)
+        p = int(gdb.parse_and_eval("p"))
+        return pwndbg.gdblib.symbol.get(p)
+
+    assert get_next_ptr() == "main"
+
+    assert get_next_ptr() == "break_here(void*)"
+
+    # Test for the bug https://github.com/pwndbg/pwndbg/issues/1600
+    assert get_next_ptr() == "A::foo(int, int)"
+
+    assert get_next_ptr() == "A::call_foo()"