about summary refs log tree commit diff
path: root/third_party/bazel/rules_haskell/debug/linking_utils/README.md
blob: 57384a27fe54a5702200de627bd1f3343b96ea3c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# Debugging linking errors

The usual utilities, like `nm`, `objdump`, and of course `ldd` (see
[here](https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/#tools-for-binary-analysis)
for a good overview of existing tools) go a long way. Yet, when
debugging non-trivial runtime linker failures one would oftentimes
like to filter outputs programmatically, with more advanced query
logic than just simple `grep` and `sed` expressions.

This library provides a small set of utility subroutines. These can
help debug complicated linker errors.

The main function is `ldd(f, elf_path)`. It is in the same spirit
as `ldd(1)`, but instead of a flat list of resolved libraries, it
returns a tree of structured information.

When we use the term `ldd` in the following document, it refers
to the `ldd` function exported from [./ldd.py](./ldd.py).

To query that tree, you pass it a function `f`, which is applied to
each dependency recursively (transforming the tree from the bottom
up).

The following functions are exported alongside the `ldd` function.
They can be passed to `ldd` and used as building blocks for insightful
queries:

- `identity`: don’t transform, output everything
- `remove_matching_needed`: remove needed entries that match a regex
- `remove_matching_runpaths`: remove runpaths that match a regex
- `non_existing_runpaths`: return a list of runpaths that don’t exist
  in the filesystem
- `unused_runpaths`: return a list of runpaths that are listed in the
  elf binary header, but no dependency was actually found in them
- `collect_unused_runpaths`: give an overview of all unused runpaths

Helpers:
- `dict_remove_empty`: remove fields with empty lists/dicts from an output
- `items`: `dict.iteritems()` for both python 2 and 3

See the introductory tutorial below on how to use these functions.

## Example usage

### Setup

If you have a bazel target which outputs a binary which you want to
debug, the easiest way is to use `ldd_test`:

```python
load(
    "//:debug/linking_utils/ldd_test.bzl",
    "ldd_test",
)

ldd_test(
    name = "test-ldd",
    elf_binary = "//tests/binary-indirect-cbits",
    current_workspace = None,
    script = r'''
YOUR SCRIPT HERE
'''
)
```

All exported functions from `ldd.py` are already in scope.
See the [`BUILD`](./BUILD) file in this directory for an example.


### Writing queries

`ldd` takes a function that is applied to each layer of elf
dependencies. This function is passed a set of structured data.
This data is gathered by querying the elf binary with `objdump`
and parsing the header fields of the dynamic section:

```
DependencyInfo :
{ needed : dict(string, union(
    LDD_MISSING, LDD_UNKNOWN,
    {
        # the needed dependency
        item : a,
        # where the dependency was found in
        found_in : RunpathDir
    }))
# all runpath directories that were searched
, runpath_dirs : [ RunpathDir ] }
```

The amount of data can get quite extensive for larger projects, so you
need a way to filter it down to get to the bottom of our problem.

If a transitive dependency cannot be found by the runtime linker, the
binary cannot be started. `ldd` shows such a problem by setting
the corresponding value in the `needed` dict to `LDD_MISSING`.
To remove everything from the output but the missing dependency and
the path to that dependency, you can write a filter like this:

```python
# `d` is the DependencyInfo dict from above
def filter_down_to_missing(d):
    res = {}

    # items is a .iteritems() that works for py 2 and 3
    for name, dep in items(d['needed']):
        if dep == LDD_MISSING:
            res[name] = LDD_MISSING
        elif dep in LDD_ERRORS:
            pass
        else:
            # dep['item'] contains the already converted info
            # from the previous layer
            res[name] = dep['item']

    # dict_remove_empty removes all empty fields from the dict,
    # otherwise your result contains a lot of {} in the values.
    return dict_remove_empty(res)

# To get human-readable output, we re-use python’s pretty printing
# library. It’s only simple python values after all!
import pprint
pprint.pprint(
  # actually parse the elf binary and apply only_missing on each layer
  ldd(
    filter_down_to_missing,
    # the path to the elf binary you want to expect.
    elf_binary_path
  )
)
```

Note that in the filter you only need to filter the data for the
current executable, and add the info from previous layers (which are
available in `d['item']`).

The result might look something like:

```python
{'libfoo.so.5': {'libbar.so.1': {'libbaz.so.6': 'MISSING'}}}
```

or

```python
{}
```

if nothing is missing.

Now, that is a similar output to what a tool like `lddtree(1)` could
give you. But we don’t need to stop there because it’s trivial to
augment your output with more information:


```python
def missing_with_runpath(d):
  # our previous function can be re-used
  missing = filter_down_to_missing(d)

  # only display runpaths if there are missing deps
  runpaths = [] if missing is {} else d['runpath_dirs']

  # dict_remove_empty keeps the output clean
  return dict_remove_empty({
    'rpth': runpaths,
    'miss': missing
  })

# same invocation, different function
pprint.pprint(
  ldd(
    missing_with_runpath,
    elf_binary_path
  )
)
```

which displays something like this for my example binary:

```python
{ 'miss': { 'libfoo.so.5': { 'miss': { 'libbar.so.1': { 'miss': { 'libbaz.so.6': 'MISSING'},
                                                          'rpth': [ { 'absolute_path': '/home/philip/.cache/bazel/_bazel_philip/fd9fea5ad581ea59473dc1f9d6bce826/execroot/myproject/bazel-out/k8-fastbuild/bin/something/and/bazel-out/k8-fastbuild/bin/other/integrate',
                                                                      'path': '$ORIGIN/../../../../../../bazel-out/k8-fastbuild/bin/other/integrate'}]}},
                             'rpth': [ { 'absolute_path': '/nix/store/xdsjx0gba4id3yyqxv66bxnm2sqixkjj-glibc-2.27/lib',
                                         'path': '/nix/store/xdsjx0gba4id3yyqxv66bxnm2sqixkjj-glibc-2.27/lib'},
                                       { 'absolute_path': '/nix/store/x6inizi5ahlyhqxxwv1rvn05a25icarq-gcc-7.3.0-lib/lib',
                                         'path': '/nix/store/x6inizi5ahlyhqxxwv1rvn05a25icarq-gcc-7.3.0-lib/lib'}]}},
  'rpth': [ … lots more nix rpaths … ]}
```

That’s still a bit cluttered for my taste, so let’s filter out
the `/nix/store` paths (which are mostly noise):

```python
import re
nix_matcher = re.compile("/nix/store.*")

def missing_with_runpath(d):
  missing = filter_down_to_missing(d)

  # this is one of the example functions provided by ldd.py
  remove_matching_runpaths(d, nix_matcher)
  # ^^^

  runpaths = [] if missing is {} else d['runpath_dirs']

  # dict_remove_empty keeps the output clean
  return dict_remove_empty({
    'rpth': runpaths,
    'miss': missing
  })
```

and we are down to:

```python
{ 'miss': { 'libfoo.so.5': { 'miss': { 'libbar.so.1': { 'miss': { 'libbaz.so.6': 'MISSING'},
                                                          'rpth': [ { 'absolute_path': '/home/philip/.cache/bazel/_bazel_philip/fd9fea5ad581ea59473dc1f9d6bce826/execroot/myproject/bazel-out/k8-fastbuild/bin/something/and/bazel-out/k8-fastbuild/bin/other/integrate',
                                                                      'path': '$ORIGIN/../../../../../../bazel-out/k8-fastbuild/bin/other/integrate'}]}}}
```

… which shows exactly the path that is missing the dependency we
expect. But what has gone wrong? Does this path even exist? We can
find out!

```python
import re
nix_matcher = re.compile("/nix/store.*")

def missing_with_runpath(d):
  missing = filter_down_to_missing(d)
  remove_matching_runpaths(d, nix_matcher)
  runpaths = [] if missing is {} else d['runpath_dirs']

  # returns a list of runpaths that don’t exist in the filesystem
  doesnt_exist = non_existing_runpaths(d)
  # ^^^

  return dict_remove_empty({
    'rpth': runpaths,
    'miss': missing,
    'doesnt_exist': doesnt_exist,
  })
```

I amended the output by a list of runpaths which point to non-existing
directories:

```python
{ 'miss': { 'libfoo.so.5': { 'miss': { 'libbar.so.1': { 'miss': { 'libbaz.so.6': 'MISSING'},
                                                        'rpth': [ { 'absolute_path': '/home/philip/.cache/bazel/_bazel_philip/fd9fea5ad581ea59473dc1f9d6bce826/execroot/myproject/bazel-out/k8-fastbuild/bin/something/and/bazel-out/k8-fastbuild/bin/other/integrate',
                                                                    'path': '$ORIGIN/../../../../../../bazel-out/k8-fastbuild/bin/other/integrate'}]
                                                        'doesnt_exist': [ { 'absolute_path': '/home/philip/.cache/bazel/_bazel_philip/fd9fea5ad581ea59473dc1f9d6bce826/execroot/myproject/bazel-out/k8-fastbuild/bin/something/and/bazel-out/k8-fastbuild/bin/other/integrate',
                                                                            'path': '$ORIGIN/../../../../../../bazel-out/k8-fastbuild/bin/other/integrate'}]}}}
```

Suddenly it’s perfectly clear where the problem lies,
`$ORIGIN/../../../../../../bazel-out/k8-fastbuild/bin/other/integrate`
points to a path that does not exist.

Any data query you’d like to do is possible, as long as it uses
the data provided by the `ldd` function. See the lower part of
`ldd.py` for more examples.