xref: /llvm-project/lldb/test/API/functionalities/load_unload/TestLoadUnload.py (revision 139df36d89bd731b5180be3cac2b58d4b2082368)
1"""
2Test that breakpoint by symbol name works correctly with dynamic libs.
3"""
4
5import os
6import re
7import lldb
8from lldbsuite.test.decorators import *
9from lldbsuite.test.lldbtest import *
10from lldbsuite.test import lldbutil
11
12
13class LoadUnloadTestCase(TestBase):
14    NO_DEBUG_INFO_TESTCASE = True
15
16    def setUp(self):
17        # Call super's setUp().
18        TestBase.setUp(self)
19        self.setup_test()
20        # Invoke the default build rule.
21        self.build()
22        # Find the line number to break for main.cpp.
23        self.line = line_number(
24            "main.cpp",
25            "// Set break point at this line for test_lldb_process_load_and_unload_commands().",
26        )
27        self.line_d_function = line_number(
28            "d.cpp", "// Find this line number within d_dunction()."
29        )
30
31    def setup_test(self):
32        lldbutil.mkdir_p(self.getBuildArtifact("hidden"))
33        if lldb.remote_platform:
34            path = lldb.remote_platform.GetWorkingDirectory()
35        else:
36            path = self.getBuildDir()
37            if self.dylibPath in os.environ:
38                sep = self.platformContext.shlib_path_separator
39                path = os.environ[self.dylibPath] + sep + path
40        self.runCmd(
41            "settings append target.env-vars '{}={}'".format(self.dylibPath, path)
42        )
43        self.default_path = path
44
45    def copy_shlibs_to_remote(self, hidden_dir=False):
46        """Copies the shared libs required by this test suite to remote.
47        Does nothing in case of non-remote platforms.
48        """
49        if lldb.remote_platform:
50            ext = "so"
51            if self.platformIsDarwin():
52                ext = "dylib"
53
54            shlibs = [
55                "libloadunload_a." + ext,
56                "libloadunload_b." + ext,
57                "libloadunload_c." + ext,
58                "libloadunload_d." + ext,
59            ]
60            wd = lldb.remote_platform.GetWorkingDirectory()
61            cwd = os.getcwd()
62            for f in shlibs:
63                err = lldb.remote_platform.Put(
64                    lldb.SBFileSpec(self.getBuildArtifact(f)),
65                    lldb.SBFileSpec(lldbutil.join_remote_paths(wd, f)),
66                )
67                if err.Fail():
68                    raise RuntimeError(
69                        "Unable copy '%s' to '%s'.\n>>> %s" % (f, wd, err.GetCString())
70                    )
71            if hidden_dir:
72                shlib = "libloadunload_d." + ext
73                hidden_dir = os.path.join(wd, "hidden")
74                hidden_file = lldbutil.join_remote_paths(hidden_dir, shlib)
75                err = lldb.remote_platform.MakeDirectory(hidden_dir)
76                if err.Fail():
77                    raise RuntimeError(
78                        "Unable to create a directory '%s'." % hidden_dir
79                    )
80                err = lldb.remote_platform.Put(
81                    lldb.SBFileSpec(os.path.join("hidden", shlib)),
82                    lldb.SBFileSpec(hidden_file),
83                )
84                if err.Fail():
85                    raise RuntimeError(
86                        "Unable copy 'libloadunload_d.so' to '%s'.\n>>> %s"
87                        % (wd, err.GetCString())
88                    )
89
90    def setSvr4Support(self, enabled):
91        self.runCmd(
92            "settings set plugin.process.gdb-remote.use-libraries-svr4 {enabled}".format(
93                enabled="true" if enabled else "false"
94            )
95        )
96
97    # libloadunload_d.so does not appear in the image list because executable
98    # dependencies are resolved relative to the debuggers PWD. Bug?
99    @expectedFailureAll(oslist=["freebsd", "linux", "netbsd"])
100    @skipIfRemote
101    @skipIfWindows  # Windows doesn't have dlopen and friends, dynamic libraries work differently
102    def test_modules_search_paths(self):
103        """Test target modules list after loading a different copy of the library libd.dylib, and verifies that it works with 'target modules search-paths add'."""
104        if self.platformIsDarwin():
105            dylibName = "libloadunload_d.dylib"
106        else:
107            dylibName = "libloadunload_d.so"
108
109        # The directory with the dynamic library we did not link to.
110        new_dir = os.path.join(self.getBuildDir(), "hidden")
111
112        old_dylib = os.path.join(self.getBuildDir(), dylibName)
113        new_dylib = os.path.join(new_dir, dylibName)
114        exe = self.getBuildArtifact("a.out")
115        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
116
117        self.expect("target modules list", substrs=[old_dylib])
118        # self.expect("target modules list -t 3",
119        #    patterns = ["%s-[^-]*-[^-]*" % self.getArchitecture()])
120        # Add an image search path substitution pair.
121        self.runCmd(
122            "target modules search-paths add %s %s" % (self.getBuildDir(), new_dir)
123        )
124
125        self.expect(
126            "target modules search-paths list", substrs=[self.getBuildDir(), new_dir]
127        )
128
129        self.expect(
130            "target modules search-paths query %s" % self.getBuildDir(),
131            "Image search path successfully transformed",
132            substrs=[new_dir],
133        )
134
135        # Obliterate traces of libd from the old location.
136        os.remove(old_dylib)
137        # Inform (DY)LD_LIBRARY_PATH of the new path, too.
138        env_cmd_string = (
139            "settings replace target.env-vars " + self.dylibPath + "=" + new_dir
140        )
141        if self.TraceOn():
142            print("Set environment to: ", env_cmd_string)
143        self.runCmd(env_cmd_string)
144        self.runCmd("settings show target.env-vars")
145
146        self.runCmd("run")
147
148        self.expect(
149            "target modules list",
150            "LLDB successfully locates the relocated dynamic library",
151            substrs=[new_dylib],
152        )
153
154    # libloadunload_d.so does not appear in the image list because executable
155    # dependencies are resolved relative to the debuggers PWD. Bug?
156    @expectedFailureAll(oslist=["freebsd", "linux", "netbsd"])
157    @expectedFailureAndroid  # wrong source file shows up for hidden library
158    @skipIfWindows  # Windows doesn't have dlopen and friends, dynamic libraries work differently
159    @skipIfDarwinEmbedded
160    def test_dyld_library_path(self):
161        """Test (DY)LD_LIBRARY_PATH after moving libd.dylib, which defines d_function, somewhere else."""
162        self.copy_shlibs_to_remote(hidden_dir=True)
163
164        exe = self.getBuildArtifact("a.out")
165        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
166
167        # Shut off ANSI color usage so we don't get ANSI escape sequences
168        # mixed in with stop locations.
169        self.dbg.SetUseColor(False)
170
171        if self.platformIsDarwin():
172            dylibName = "libloadunload_d.dylib"
173            dsymName = "libloadunload_d.dylib.dSYM"
174        else:
175            dylibName = "libloadunload_d.so"
176
177        # The directory to relocate the dynamic library and its debugging info.
178        special_dir = "hidden"
179        if lldb.remote_platform:
180            wd = lldb.remote_platform.GetWorkingDirectory()
181        else:
182            wd = self.getBuildDir()
183
184        old_dir = wd
185        new_dir = os.path.join(wd, special_dir)
186        old_dylib = os.path.join(old_dir, dylibName)
187
188        # For now we don't track (DY)LD_LIBRARY_PATH, so the old
189        # library will be in the modules list.
190        self.expect(
191            "target modules list", substrs=[os.path.basename(old_dylib)], matching=True
192        )
193
194        lldbutil.run_break_set_by_file_and_line(
195            self, "d.cpp", self.line_d_function, num_expected_locations=1
196        )
197        # After run, make sure the non-hidden library is picked up.
198        self.expect("run", substrs=["return", "700"])
199
200        self.runCmd("continue")
201
202        # Add the hidden directory first in the search path.
203        env_cmd_string = "settings set target.env-vars %s=%s%s%s" % (
204            self.dylibPath,
205            new_dir,
206            self.platformContext.shlib_path_separator,
207            self.default_path,
208        )
209        self.runCmd(env_cmd_string)
210
211        # This time, the hidden library should be picked up.
212        self.expect("run", substrs=["return", "12345"])
213
214    @expectedFailureAll(
215        bugnumber="llvm.org/pr25805", hostoslist=["windows"], triple=".*-android"
216    )
217    @expectedFailureAll(oslist=["windows"])  # process load not implemented
218    def test_lldb_process_load_and_unload_commands(self):
219        self.setSvr4Support(False)
220        self.run_lldb_process_load_and_unload_commands()
221
222    @expectedFailureAll(
223        bugnumber="llvm.org/pr25805", hostoslist=["windows"], triple=".*-android"
224    )
225    @expectedFailureAll(oslist=["windows"])  # process load not implemented
226    def test_lldb_process_load_and_unload_commands_with_svr4(self):
227        self.setSvr4Support(True)
228        self.run_lldb_process_load_and_unload_commands()
229
230    def run_lldb_process_load_and_unload_commands(self):
231        """Test that lldb process load/unload command work correctly."""
232        self.copy_shlibs_to_remote()
233
234        exe = self.getBuildArtifact("a.out")
235        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
236
237        # Break at main.cpp before the call to dlopen().
238        # Use lldb's process load command to load the dylib, instead.
239
240        lldbutil.run_break_set_by_file_and_line(
241            self, "main.cpp", self.line, num_expected_locations=1, loc_exact=True
242        )
243
244        self.runCmd("run", RUN_SUCCEEDED)
245
246        ctx = self.platformContext
247        dylibName = ctx.shlib_prefix + "loadunload_a." + ctx.shlib_extension
248        localDylibPath = self.getBuildArtifact(dylibName)
249        if lldb.remote_platform:
250            wd = lldb.remote_platform.GetWorkingDirectory()
251            remoteDylibPath = lldbutil.join_remote_paths(wd, dylibName)
252        else:
253            remoteDylibPath = localDylibPath
254
255        # First make sure that we get some kind of error if process load fails.
256        # We print some error even if the load fails, which isn't formalized.
257        # The only plugin at present (Posix) that supports this says "unknown reasons".
258        # If another plugin shows up, let's require it uses "unknown error" as well.
259        non_existant_shlib = "/NoSuchDir/NoSuchSubdir/ReallyNo/NotAFile"
260        self.expect(
261            "process load %s" % (non_existant_shlib),
262            error=True,
263            matching=False,
264            patterns=["unknown reasons"],
265        )
266
267        # Make sure that a_function does not exist at this point.
268        self.expect(
269            "image lookup -n a_function",
270            "a_function should not exist yet",
271            error=True,
272            matching=False,
273            patterns=["1 match found"],
274        )
275
276        # Use lldb 'process load' to load the dylib.
277        self.expect(
278            "process load %s --install=%s" % (localDylibPath, remoteDylibPath),
279            "%s loaded correctly" % dylibName,
280            patterns=[
281                'Loading "%s".*ok' % re.escape(localDylibPath),
282                "Image [0-9]+ loaded",
283            ],
284        )
285
286        # Search for and match the "Image ([0-9]+) loaded" pattern.
287        output = self.res.GetOutput()
288        pattern = re.compile("Image ([0-9]+) loaded")
289        for l in output.split(os.linesep):
290            self.trace("l:", l)
291            match = pattern.search(l)
292            if match:
293                break
294        index = match.group(1)
295
296        # Now we should have an entry for a_function.
297        self.expect(
298            "image lookup -n a_function",
299            "a_function should now exist",
300            patterns=["1 match found .*%s" % dylibName],
301        )
302
303        # Use lldb 'process unload' to unload the dylib.
304        self.expect(
305            "process unload %s" % index,
306            "%s unloaded correctly" % dylibName,
307            patterns=["Unloading .* with index %s.*ok" % index],
308        )
309
310        # Confirm that we unloaded properly.
311        self.expect(
312            "image lookup -n a_function",
313            "a_function should not exist after unload",
314            error=True,
315            matching=False,
316            patterns=["1 match found"],
317        )
318
319        self.runCmd("process continue")
320
321    @expectedFailureAll(oslist=["windows"])  # breakpoint not hit
322    def test_load_unload(self):
323        self.setSvr4Support(False)
324        self.run_load_unload()
325
326    @expectedFailureAll(oslist=["windows"])  # breakpoint not hit
327    def test_load_unload_with_svr4(self):
328        self.setSvr4Support(True)
329        self.run_load_unload()
330
331    def run_load_unload(self):
332        """Test breakpoint by name works correctly with dlopen'ing."""
333        self.copy_shlibs_to_remote()
334
335        exe = self.getBuildArtifact("a.out")
336        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
337
338        # Break by function name a_function (not yet loaded).
339        lldbutil.run_break_set_by_symbol(self, "a_function", num_expected_locations=0)
340
341        self.runCmd("run", RUN_SUCCEEDED)
342
343        # The stop reason of the thread should be breakpoint and at a_function.
344        self.expect(
345            "thread list",
346            STOPPED_DUE_TO_BREAKPOINT,
347            substrs=["stopped", "a_function", "stop reason = breakpoint"],
348        )
349
350        # The breakpoint should have a hit count of 1.
351        lldbutil.check_breakpoint(self, bpno=1, expected_hit_count=1)
352
353        # Issue the 'continue' command.  We should stop again at a_function.
354        # The stop reason of the thread should be breakpoint and at a_function.
355        self.runCmd("continue")
356
357        # rdar://problem/8508987
358        # The a_function breakpoint should be encountered twice.
359        self.expect(
360            "thread list",
361            STOPPED_DUE_TO_BREAKPOINT,
362            substrs=["stopped", "a_function", "stop reason = breakpoint"],
363        )
364
365        # The breakpoint should have a hit count of 2.
366        lldbutil.check_breakpoint(self, bpno=1, expected_hit_count=2)
367
368    def test_step_over_load(self):
369        self.setSvr4Support(False)
370        self.run_step_over_load()
371
372    def test_step_over_load_with_svr4(self):
373        self.setSvr4Support(True)
374        self.run_step_over_load()
375
376    def run_step_over_load(self):
377        """Test stepping over code that loads a shared library works correctly."""
378        self.copy_shlibs_to_remote()
379
380        exe = self.getBuildArtifact("a.out")
381        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
382
383        # Break by function name a_function (not yet loaded).
384        lldbutil.run_break_set_by_file_and_line(
385            self, "main.cpp", self.line, num_expected_locations=1, loc_exact=True
386        )
387
388        self.runCmd("run", RUN_SUCCEEDED)
389
390        # The stop reason of the thread should be breakpoint and at a_function.
391        self.expect(
392            "thread list",
393            STOPPED_DUE_TO_BREAKPOINT,
394            substrs=["stopped", "stop reason = breakpoint"],
395        )
396
397        self.runCmd("thread step-over", "Stepping over function that loads library")
398
399        # The stop reason should be step end.
400        self.expect(
401            "thread list",
402            "step over succeeded.",
403            substrs=["stopped", "stop reason = step over"],
404        )
405
406    # We can't find a breakpoint location for d_init before launching because
407    # executable dependencies are resolved relative to the debuggers PWD. Bug?
408    # The remote lldb server resolves the executable dependencies correctly.
409    @expectedFailureAll(
410        oslist=["freebsd", "linux", "netbsd"],
411        remote=False,
412    )
413    @expectedFailureAll(oslist=["windows"], archs=["aarch64"])
414    def test_static_init_during_load(self):
415        """Test that we can set breakpoints correctly in static initializers"""
416        self.copy_shlibs_to_remote()
417
418        exe = self.getBuildArtifact("a.out")
419        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
420
421        a_init_bp_num = lldbutil.run_break_set_by_symbol(
422            self, "a_init", num_expected_locations=0
423        )
424        b_init_bp_num = lldbutil.run_break_set_by_symbol(
425            self, "b_init", num_expected_locations=0
426        )
427        d_init_bp_num = lldbutil.run_break_set_by_symbol(
428            self, "d_init", num_expected_locations=1
429        )
430
431        self.runCmd("run", RUN_SUCCEEDED)
432
433        self.expect(
434            "thread list",
435            STOPPED_DUE_TO_BREAKPOINT,
436            substrs=[
437                "stopped",
438                "d_init",
439                "stop reason = breakpoint %d" % d_init_bp_num,
440            ],
441        )
442
443        self.runCmd("continue")
444        self.expect(
445            "thread list",
446            STOPPED_DUE_TO_BREAKPOINT,
447            substrs=[
448                "stopped",
449                "b_init",
450                "stop reason = breakpoint %d" % b_init_bp_num,
451            ],
452        )
453        self.expect("thread backtrace", substrs=["b_init", "dylib_open", "main"])
454
455        self.runCmd("continue")
456        self.expect(
457            "thread list",
458            STOPPED_DUE_TO_BREAKPOINT,
459            substrs=[
460                "stopped",
461                "a_init",
462                "stop reason = breakpoint %d" % a_init_bp_num,
463            ],
464        )
465        self.expect("thread backtrace", substrs=["a_init", "dylib_open", "main"])
466