Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Remove debug symbols from build #17678

Merged
merged 3 commits into from
Nov 2, 2022
Merged

Conversation

swenson
Copy link
Contributor

@swenson swenson commented Oct 26, 2022

Proposal: Remove debug symbols from release builds

By adding the link flags -s -w we can reduce the Vault binary size from 204 MB to 167 MB (about 18% reduction in size).

What does this doe?

This removes the DWARF section of the binary.

i.e., before:

$ objdump --section-headers vault-debug

vault-debug:	file format mach-o arm64

Sections:
Idx Name             Size     VMA              Type
  0 __text           03a00340 0000000100001000 TEXT
  1 __symbol_stub1   00000618 0000000103a01340 TEXT
  2 __rodata         00c18088 0000000103a01960 DATA
  3 __rodata         015aee18 000000010461c000 DATA
  4 __typelink       0004616c 0000000105bcae20 DATA
  5 __itablink       0000eb68 0000000105c10fa0 DATA
  6 __gosymtab       00000000 0000000105c1fb08 DATA
  7 __gopclntab      02a5b8e0 0000000105c1fb20 DATA
  8 __go_buildinfo   00008c10 000000010867c000 DATA
  9 __nl_symbol_ptr  00000410 0000000108684c10 DATA
 10 __noptrdata      000fed00 0000000108685020 DATA
 11 __data           0004e1f0 0000000108783d20 DATA
 12 __bss            00052520 00000001087d1f20 BSS
 13 __noptrbss       000151b0 0000000108824440 BSS
 14 __zdebug_abbrev  00000129 000000010883c000 DATA, DEBUG
 15 __zdebug_line    00651374 000000010883c129 DATA, DEBUG
 16 __zdebug_frame   001e1de9 0000000108e8d49d DATA, DEBUG
 17 __debug_gdb_scri 00000043 000000010906f286 DATA, DEBUG
 18 __zdebug_info    00de2c09 000000010906f2c9 DATA, DEBUG
 19 __zdebug_loc     00a619ea 0000000109e51ed2 DATA, DEBUG
 20 __zdebug_ranges  001e94a6 000000010a8b38bc DATA, DEBUG

And after:

$ objdump --section-headers vault-no-debug

vault-no-debug:	file format mach-o arm64

Sections:
Idx Name            Size     VMA              Type
  0 __text          03a00340 0000000100001000 TEXT
  1 __symbol_stub1  00000618 0000000103a01340 TEXT
  2 __rodata        00c18088 0000000103a01960 DATA
  3 __rodata        015aee18 000000010461c000 DATA
  4 __typelink      0004616c 0000000105bcae20 DATA
  5 __itablink      0000eb68 0000000105c10fa0 DATA
  6 __gosymtab      00000000 0000000105c1fb08 DATA
  7 __gopclntab     02a5b8e0 0000000105c1fb20 DATA
  8 __go_buildinfo  00008c20 000000010867c000 DATA
  9 __nl_symbol_ptr 00000410 0000000108684c20 DATA
 10 __noptrdata     000fed00 0000000108685040 DATA
 11 __data          0004e1f0 0000000108783d40 DATA
 12 __bss           00052520 00000001087d1f40 BSS
 13 __noptrbss      000151b0 0000000108824460 BSS

What does this affect?

The only side effect I have been able to find is that it is no longer possible to use delve to run the Vault release binary.

Note, however, that running delve and other debuggers requires access to the full source code, which isn't provided for the Enterprise, HSM, etc. binaries, so it isn't possible to debug those anyway outside of people who have the full source.

What does this not affect?

  • panic traces
  • vault debug
  • error messages
  • Despite what the documentation says, these flags do not delete the function symbol table (so it is not the same as having a stripped binary).

Okay, but what is in the removed sections?

The DWARF sections contain mappings between the compiled binary and functions, parameters, and variables in the source code.

Using llvm-dwarfdump, it looks like:

0x011a6d85:   DW_TAG_subprogram
                DW_AT_name	("github.com/hashicorp/vault/api.(*replicationStateStore).recordState")
                DW_AT_low_pc	(0x0000000000a99300)
                DW_AT_high_pc	(0x0000000000a99419)
                DW_AT_frame_base	(DW_OP_call_frame_cfa)
                DW_AT_decl_file	("/home/swenson/vault/api/client.go")
                DW_AT_external	(0x01)

0x011a6de1:     DW_TAG_formal_parameter
                  DW_AT_name	("w")
                  DW_AT_variable_parameter	(0x00)
                  DW_AT_decl_line	(1735)
                  DW_AT_type	(0x00000000001e834a "github.com/hashicorp/vault/api.replicationStateStore *")
                  DW_AT_location	(0x009e832a:
                     [0x0000000000a99300, 0x0000000000a9933a): DW_OP_reg0 RAX
                     [0x0000000000a9933a, 0x0000000000a99419): DW_OP_call_frame_cfa)

0x011a6def:     DW_TAG_formal_parameter
                  DW_AT_name	("resp")
                  DW_AT_variable_parameter	(0x00)
                  DW_AT_decl_line	(1735)
                  DW_AT_type	(0x00000000001e82a2 "github.com/hashicorp/vault/api.Response *")
                  DW_AT_location	(0x009e8370:
                     [0x0000000000a99300, 0x0000000000a9933a): DW_OP_reg3 RBX
                     [0x0000000000a9933a, 0x0000000000a99419): DW_OP_fbreg +8)

0x011a6e00:     DW_TAG_variable
                  DW_AT_name	("newState")
                  DW_AT_decl_line	(1738)
                  DW_AT_type	(0x0000000000119f32 "string")
                  DW_AT_location	(0x009e83b7:
                     [0x0000000000a99385, 0x0000000000a99385): DW_OP_reg0 RAX, DW_OP_piece 0x8, DW_OP_piece 0x8
                     [0x0000000000a99385, 0x0000000000a993a4): DW_OP_reg0 RAX, DW_OP_piece 0x8, DW_OP_reg3 RBX, DW_OP_piece 0x8
                     [0x0000000000a993a4, 0x0000000000a993a7): DW_OP_piece 0x8, DW_OP_reg3 RBX, DW_OP_piece 0x8)

This says that the particular binary section is the function github.com/hashicorp/vault/api.(*replicationStateStore).recordState, from the file /home/swenson/vault/api/client.go, containing the w parameter on line 1735 mapped to certain registers and memory, the resp parameter on line 1735 mapped to certain registers and memory, and the newState variable on line 1738, mapped to certain registers, and memory.

It's really only useful for a debugger.

You can see some details in the debug/dwarf docs.

What if I need to run in a debugger?

Anyone running the code in a debugger will need full access the source code anyway, so presumably they will be able to run make dev and build the version with the DWARF sections intact, and then run their debugger.

Initially I had proposed releasing a separate +debug binary, but I've realized that I don't think this would actually be useful to anyone?

@@ -0,0 +1,3 @@
```release-note:improvement
Remove debug symbols from release binaries.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the writeup, we expect the primary externally-visible change to be "reduced binary size", and I think that might be a better way of describing the change in the changelog.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@mladlow
Copy link
Collaborator

mladlow commented Oct 26, 2022

This is a great writeup @swenson and addresses my primary concern but I'm going to defer actual reviewing duties to the other reviewers.

Copy link
Contributor

@tomhjp tomhjp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your reasoning, this is a great improvement 👍

@@ -258,7 +258,7 @@ ci-verify:
# This is used for release builds by .github/workflows/build.yml
build:
@echo "--> Building Vault $(VAULT_VERSION)"
@go build -v -tags "$(GO_TAGS)" -ldflags " -X github.com/hashicorp/vault/sdk/version.Version=$(VAULT_VERSION) -X github.com/hashicorp/vault/sdk/version.GitCommit=$(VAULT_REVISION) -X github.com/hashicorp/vault/sdk/version.BuildDate=$(VAULT_BUILD_DATE)" -o dist/
@go build -v -tags "$(GO_TAGS)" -ldflags " -s -w -X github.com/hashicorp/vault/sdk/version.Version=$(VAULT_VERSION) -X github.com/hashicorp/vault/sdk/version.GitCommit=$(VAULT_REVISION) -X github.com/hashicorp/vault/sdk/version.BuildDate=$(VAULT_BUILD_DATE)" -o dist/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think technically the -w is redundant, as -s is a superset. In my testing both -s and -s -w produced byte-for-byte identical results on a different package, but no harm in keeping it. [ref]

-s
Omit the symbol table and debug information.

-w
Omit the DWARF symbol table.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this in my testing as well. All of the recommendations I've found say to use both, so I did.

Copy link
Contributor

@austingebauer austingebauer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice description! Took a survey of some other products and it looks like at least Terraform, Waypoint, and Packer are also doing this. LGTM 👍

@swenson
Copy link
Contributor Author

swenson commented Nov 1, 2022

Thanks for the +1s. Any thoughts @jasonodonnell @ncabatoff @sgmiller?

Copy link
Contributor

@jasonodonnell jasonodonnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only concern was would this prevent us from using pprof, but that doesn't seem to be the case, so 👍.

Christopher Swenson added 3 commits November 1, 2022 09:39
By adding the link flags `-s -w` we can reduce the Vault binary size
from 204 MB to 167 MB (about 18% reduction in size).

This removes the DWARF section of the binary.

i.e., before:

```
$ objdump --section-headers vault-debug

vault-debug:	file format mach-o arm64

Sections:
Idx Name             Size     VMA              Type
  0 __text           03a00340 0000000100001000 TEXT
  1 __symbol_stub1   00000618 0000000103a01340 TEXT
  2 __rodata         00c18088 0000000103a01960 DATA
  3 __rodata         015aee18 000000010461c000 DATA
  4 __typelink       0004616c 0000000105bcae20 DATA
  5 __itablink       0000eb68 0000000105c10fa0 DATA
  6 __gosymtab       00000000 0000000105c1fb08 DATA
  7 __gopclntab      02a5b8e0 0000000105c1fb20 DATA
  8 __go_buildinfo   00008c10 000000010867c000 DATA
  9 __nl_symbol_ptr  00000410 0000000108684c10 DATA
 10 __noptrdata      000fed00 0000000108685020 DATA
 11 __data           0004e1f0 0000000108783d20 DATA
 12 __bss            00052520 00000001087d1f20 BSS
 13 __noptrbss       000151b0 0000000108824440 BSS
 14 __zdebug_abbrev  00000129 000000010883c000 DATA, DEBUG
 15 __zdebug_line    00651374 000000010883c129 DATA, DEBUG
 16 __zdebug_frame   001e1de9 0000000108e8d49d DATA, DEBUG
 17 __debug_gdb_scri 00000043 000000010906f286 DATA, DEBUG
 18 __zdebug_info    00de2c09 000000010906f2c9 DATA, DEBUG
 19 __zdebug_loc     00a619ea 0000000109e51ed2 DATA, DEBUG
 20 __zdebug_ranges  001e94a6 000000010a8b38bc DATA, DEBUG
```

And after:

```
$ objdump --section-headers vault-no-debug

vault-no-debug:	file format mach-o arm64

Sections:
Idx Name            Size     VMA              Type
  0 __text          03a00340 0000000100001000 TEXT
  1 __symbol_stub1  00000618 0000000103a01340 TEXT
  2 __rodata        00c18088 0000000103a01960 DATA
  3 __rodata        015aee18 000000010461c000 DATA
  4 __typelink      0004616c 0000000105bcae20 DATA
  5 __itablink      0000eb68 0000000105c10fa0 DATA
  6 __gosymtab      00000000 0000000105c1fb08 DATA
  7 __gopclntab     02a5b8e0 0000000105c1fb20 DATA
  8 __go_buildinfo  00008c20 000000010867c000 DATA
  9 __nl_symbol_ptr 00000410 0000000108684c20 DATA
 10 __noptrdata     000fed00 0000000108685040 DATA
 11 __data          0004e1f0 0000000108783d40 DATA
 12 __bss           00052520 00000001087d1f40 BSS
 13 __noptrbss      000151b0 0000000108824460 BSS
```

The only side effect I have been able to find is that it is no longer
possible to use [delve](https://github.com/go-delve/delve) to run the
Vault binary.

Note, however, that running delve and other debuggers requires access
to the full source code, which isn't provided for the Enterprise, HSM,
etc. binaries, so it isn't possible to debug those anyway outside of
people who have the full source.

* panic traces
* `vault debug`
* error messages
* Despite what the documentation says, these flags do *not* delete the
function symbol table (so it is not the same as having a `strip`ped
binary).

It contains mappings between the compiled binary and functions,
paramters, and variables in the source code.

Using `llvm-dwarfdump`, it looks like:

```
0x011a6d85:   DW_TAG_subprogram
                DW_AT_name	("github.com/hashicorp/vault/api.(*replicationStateStore).recordState")
                DW_AT_low_pc	(0x0000000000a99300)
                DW_AT_high_pc	(0x0000000000a99419)
                DW_AT_frame_base	(DW_OP_call_frame_cfa)
                DW_AT_decl_file	("/home/swenson/vault/api/client.go")
                DW_AT_external	(0x01)

0x011a6de1:     DW_TAG_formal_parameter
                  DW_AT_name	("w")
                  DW_AT_variable_parameter	(0x00)
                  DW_AT_decl_line	(1735)
                  DW_AT_type	(0x00000000001e834a "github.com/hashicorp/vault/api.replicationStateStore *")
                  DW_AT_location	(0x009e832a:
                     [0x0000000000a99300, 0x0000000000a9933a): DW_OP_reg0 RAX
                     [0x0000000000a9933a, 0x0000000000a99419): DW_OP_call_frame_cfa)

0x011a6def:     DW_TAG_formal_parameter
                  DW_AT_name	("resp")
                  DW_AT_variable_parameter	(0x00)
                  DW_AT_decl_line	(1735)
                  DW_AT_type	(0x00000000001e82a2 "github.com/hashicorp/vault/api.Response *")
                  DW_AT_location	(0x009e8370:
                     [0x0000000000a99300, 0x0000000000a9933a): DW_OP_reg3 RBX
                     [0x0000000000a9933a, 0x0000000000a99419): DW_OP_fbreg +8)

0x011a6e00:     DW_TAG_variable
                  DW_AT_name	("newState")
                  DW_AT_decl_line	(1738)
                  DW_AT_type	(0x0000000000119f32 "string")
                  DW_AT_location	(0x009e83b7:
                     [0x0000000000a99385, 0x0000000000a99385): DW_OP_reg0 RAX, DW_OP_piece 0x8, DW_OP_piece 0x8
                     [0x0000000000a99385, 0x0000000000a993a4): DW_OP_reg0 RAX, DW_OP_piece 0x8, DW_OP_reg3 RBX, DW_OP_piece 0x8
                     [0x0000000000a993a4, 0x0000000000a993a7): DW_OP_piece 0x8, DW_OP_reg3 RBX, DW_OP_piece 0x8)
```

This says that the particular binary section is the function
`github.com/hashicorp/vault/api.(*replicationStateStore).recordState`,
from the file `/home/swenson/vault/api/client.go`, containing
the `w` parameter on line 1735 mapped to certain registers and memory,
the `resp` paramter on line 1735 mapped to certain reigsters and memory,
and the `newState` variable on line 1738, mapped to certain registers,
and memory.

It's really only useful for a debugger.

Anyone running the code in a debugger will need full access the source
code anyway, so presumably they will be able to run `make dev` and build
the version with the DWARF sections intact, and then run their debugger.
@swenson
Copy link
Contributor Author

swenson commented Nov 2, 2022

Okay, with no further objections...

Thanks everyone!

@swenson swenson merged commit 707111b into main Nov 2, 2022
@swenson swenson deleted the remove-debug-symbols branch November 2, 2022 17:47
@Foxboron
Copy link

Foxboron commented Jan 3, 2023

This change was reverted with fc9dfa2

Generally the above change, and this change, would have been slightly bad for Linux distributions that packages vault. Generally would recommend to keep a distinction between "release builds" and "development builds".

@swenson
Copy link
Contributor Author

swenson commented Jan 3, 2023

@Foxboron not sure what you mean. We don't provide development builds, only release builds, so it should not be expected to have all of the DWARF debug information.

@Foxboron
Copy link

Foxboron commented Jan 3, 2023

@swenson I actually somehow missed it, but you do provide development builds with the new Makefile through make dev. But there doesn't seem to be any difference between make bin and make dev casually browsing the build scripts.

Why would -s -w presumably be explicitly passed as part of the building instead of leaving that up to the packaging steps through strip? This would leave you with the option to build debug packages for internal infrastructure in the long run.

@swenson
Copy link
Contributor Author

swenson commented Jan 3, 2023

@Foxboron (The change was not reverted, btw -- I just confirmed, and we still pass -s -w to ldflags in the new build system, and this change should take effect in Vault 1.13.0).

make dev and make bin are essentially the same now, and both include debugging information.

The release process uses make ci-build, which removes the debugging information.

Go debug binaries are not generally useful unless you have the complete source code associated with a build, and are only useful for stepping through with a debugger. So, presumably, if a user has gotten to that point of troubleshooting an issue, they'll have a development environment and are able to run make dev themselves, and build the debug binary as needed.

Passing -s -w to the ldflags is not the same as strip. The -s -w doesn't actually remove all symbols, but mostly just the DWARF sections containing mappings of the binary to variables, line numbers, parameters, etc. Running strip would remove a lot more symbols, and might cause problems for us (though I have not verified this).

@Foxboron
Copy link

Foxboron commented Jan 3, 2023

(The change was not reverted, btw -- I just confirmed, and we still pass -s -w to ldflags in the new build system, and this change should take effect in Vault 1.13.0).

Right, I missed that there was two scripts building binaries. Sorry.

Go debug binaries are not generally useful unless you have the complete source code associated with a build, and are only useful for stepping through with a debugger. So, presumably, if a user has gotten to that point of troubleshooting an issue, they'll have a development environment and are able to run make dev themselves, and build the debug binary as needed.

Arch Linux would provide complete debug packages for vault through debuginfod and delve in the future. Passing -s -w as default options would break this feature, which is what prompted me to look at this.

Passing -s -w to the ldflags is not the same as strip. The -s -w doesn't actually remove all symbols, but mostly just the DWARF sections containing mappings of the binary to variables, line numbers, parameters, etc. Running strip would remove a lot more symbols, and might cause problems for us (though I have not verified this).

Mm, I'm not aware of any issues running strip on Go binaries. It should be fine I think? I'd be interested to learn about any edge cases if you do find them though.

@swenson
Copy link
Contributor Author

swenson commented Jan 3, 2023

For debug packages for Arch, I'd probably recommend building them with make dev in the future.

I assumed strip would affect panic backtraces, but I checked and they are not affected. It's probably safe to use, but I'd want to do further research before implementing it for us :)

It is most beneficial to use -s -w with ldflags and strip to get the most space savings -- strip doesn't remove the DWARF sections from the binary.

@Foxboron
Copy link

Foxboron commented Jan 3, 2023

It is most beneficial to use -s -w with ldflags and strip to get the most space savings -- strip doesn't remove the DWARF sections from the binary.

Which sections doesn't strip remove? It should remove everything.

λ delve master» go build ./cmd/dlv
λ delve master» strip dlv
λ delve master» du -s dlv
12328	dlv
λ delve master» rm dlv
λ delve master» go build -ldflags '-s -w' ./cmd/dlv
λ delve master» du -s dlv
12328	dlv

@swenson
Copy link
Contributor Author

swenson commented Jan 3, 2023

At least on macOS, they are very different:

[swenson@hashicorp 10:11:53] vault :) $ make dev
==> Checking that build is using go version >= 1.19.4...
==> Using go version 1.19.4...
==> Removing old directory...
==> Building...

==> Results:
total 404032
-rwxr-xr-x  1 swenson  staff   197M Jan  3 10:12 vault
[swenson@hashicorp 10:12:11] vault :) $ du -s bin/vault
404032	bin/vault
[swenson@hashicorp 10:12:13] vault :) $ strip bin/vault
[swenson@hashicorp 10:12:19] vault :) $ du -s bin/vault
365616	bin/vault

[swenson@hashicorp 10:12:23] vault :) $ make dev LD_FLAGS="-s -w "
==> Checking that build is using go version >= 1.19.4...
==> Using go version 1.19.4...
==> Removing old directory...
==> Building...

==> Results:
total 331888
-rwxr-xr-x  1 swenson  staff   162M Jan  3 10:13 vault
[swenson@hashicorp 10:13:29] vault :) $ du -s bin/vault
331888	bin/vault
[swenson@hashicorp 10:13:32] vault :) $ strip bin/vault
[swenson@hashicorp 10:13:35] vault :) $ du -s bin/vault
293472	bin/vault

A quick look at objdump shows strip not removing the debug sections.

@swenson
Copy link
Contributor Author

swenson commented Jan 3, 2023

(I suspect this is Apple's strip not doing as much as Linux's. I checked on Ubuntu and strip and setting ldflags to -s -w are much closer, only a few kilobytes different.)

@Foxboron
Copy link

Foxboron commented Jan 3, 2023

λ Go » git clone git@github.com:hashicorp/vault.git
Cloning into 'vault'...
remote: Enumerating objects: 269376, done.
remote: Counting objects: 100% (159/159), done.
remote: Compressing objects: 100% (123/123), done.
remote: Total 269376 (delta 57), reused 109 (delta 36), pack-reused 269217
Receiving objects: 100% (269376/269376), 215.57 MiB | 3.58 MiB/s, done.
Resolving deltas: 100% (175396/175396), done.
λ Go » cd vault
λ vault main» make dev LD_FLAGS="-s -w "
==> Checking that build is using go version >= 1.19.4...
==> Using go version 1.19.4...
go: downloading github.com/fatih/structs v1.1.0
go: downloading github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32
....
==> Removing old directory...
==> Building...

==> Results:
total 145M
-rwxr-xr-x 1 fox fox 145M Jan  3 19:20 vault
λ vault main» du -s bin/vault
147784	bin/vault
λ vault main» strip bin/vault
λ vault main» du -s bin/vault
147784	bin/vault

λ vault main» make dev
==> Checking that build is using go version >= 1.19.4...
==> Using go version 1.19.4...
==> Removing old directory...
==> Building...

==> Results:
total 200M
-rwxr-xr-x 1 fox fox 200M Jan  3 19:21 vault
λ vault main» du -s bin/vault
204512	bin/vault
λ vault main» strip bin/vault
λ vault main» du -s bin/vault
147784	bin/vault

I don't know how golang nor strip works on OSX. But I get identical results on my machine. I'm fairly certain there is nothing wonkey with my setup.

I don't understand how Ubuntu would be different either....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants