Using -w and -s Flags in Golang
Today’s blog article comes from Valery, one of Spiral Scout’s senior software engineers who specialize in Golang (Go). As a software development agency with expertise in Golang as well as a number of other programming languages, we know it’s important for our engineers and quality assurance professionals to be able to share their knowledge and experience with our outside community. Thanks to Valery for this great post and helpful Golang testing tips!
While surfing GitHub in search of good engineering practices to adopt, I noticed a recurring theme among developers compiling their programs in Go. Many of them were using linker flags to decrease the output file volume, specifically -w and -s flags at the same time with overlapping effects.
In software testing, flags are also known as arguments or parameters. They are used to identify a specific status or condition when running a program from the command line. Flags can either be turned on or turned off, and they are applied in various languages and frameworks throughout software development.
This article is dedicated to explaining the effects of implementing -w and -s flags in Go and offering ways they can be used more efficiently.
How -w and -s Flags Work With DWARF and ELF
A quick note about the system I test on before I discuss when and how -w and -s flags are used; the hardware/software combo I work with includes:
- A Dell XPS 9570 laptop
- Manjaro Linux OS
- Testing branch
-w and -s flags are usually used at the stage of App linking in conjunction with the -ldflags directive (see https://golang.org/src/cmd/go/alldocs.go) at the stage of compilation in Go. More info about flags can be found here: https://golang.org/cmd/link/.
Before we take a closer look at the -w flag and break down the binary code to check if the DWARF symbol table disappears or not, I recommend defining the DWARF symbol table.
DWARF is a debugging data format that can be included in a binary file. According to the DWARF Wiki entry, this format was developed along with the standard common file format called ELF (Executable and Linkable Format). This article does a great job of uncovering how the debugger works with this table.
Golang creators have also shared more information on DWARF in Go source codes, including the specifics of how this table is formed and embedded in a binary file written in Go.
I’ll touch upon some of the main points below with my sample code.
First, we want to read DWARF using these steps:
1. Compile a program in Go (just with the go build command for a start).
2. Read the symbol table. This is convenient to do with readelf -Ws. You can also read the headers with something more familiar, however, such as objdump -h.
3. Note the headers of the resulting program:
We can see that the binary file includes data for debugging (from section 24 to 32 inclusive) and there is also a symbol and string table. (specified below)
4. Read the table using this command:
objdump — dwarf=info main
The output is likely to be rather long, so I saved the stdout output in the text file with the following command:
objdump — dwarf=info main &> main.txt
Below you can find a section of the output:
To find the necessary function by address, we need to know the PC (program counter). You can find the PC in the EIP register; it is represented by DW_AT_low_pc and DW_AT_high_pc. For example, use low_pc for the main.main function (main being a Go runtime function) and try to find it in a binary file using objdump -d at 0x44f930.
Good. Now let’s compile a program with the -w flag and compare it with a program compiled without one.
5. Run the following command:
go build -ldflags=”-w” -o build_with_w cmd/main.go
Then take a look at what has changed in the headers:
As we can see, the .zdebug section is completely gone. We can also accurately calculate how much smaller the binary file has become by subtracting the lower address (Off column) from the top one. When you convert that difference from bytes to kilobytes, for example, you can understand the economy even better.
In this case, the total weight of the binary file is about 25 megabytes meaning we saved roughly 3.7 kilobytes. It makes me wonder what would happen if we try to run dvl with the Delve Go debugger tool?
dlv — listen=:43671 — headless=true — api-version=2 — accept-multiclient exec ./build_with_w
…and it returns your expected result:
API server listening at: [::]:43671 could not launch process: could not open debug info
Well, now things are much clearer with the DWARF table and the -w flag!
Let’s move on to the -s flag. According to the documentation, the -s flag removes not only the information for debugging but also a specified symbol table. But how different is it from the -w flag?
First, a quick background — a symbol table contains information about local and global variables, function names, etc. In the picture above, this information is presented in sections 26 and 27(.symtab and .strtab). More detail about symbol tables can be found here: http://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html and http://refspecs.linuxbase.org/elf/gabi4+/ch4.strtab.html.
Let’s try compiling a binary file with the -s flag this time:
As expected, the information about DWARF has disappeared, as well as the sections with a symbol and string table (a kind of release flag).
What Does it Mean?
If you want to remove debugging information only, it makes the most sense to only use the -w flag. If you would like to additionally delete a symbol and string table to make the binary file smaller, use the -s flag.
Here are some helpful examples of what not to do when using these flags in Golang. While two flags may seem better than one, when it comes to -w and -s flags, that is simply not the case: