Hello everyone, I am a programmer who loves to share. I am happy to share my experiences and insights from my work.
-begin-
In embedded Linux development, dynamic libraries (.so files) are an important way to achieve code reuse. However, by default, dynamic libraries export all global symbols (functions, variables), which not only increases the size of the library but may also lead to symbol conflicts (e.g., functions with the same name in different libraries).The -fvisibility option can control the visibility of symbols, exporting only the necessary symbols to achieve “lightweight” and “isolation” of dynamic libraries.
Function of the option:
-fvisibility=visibility is used to specify the default visibility of global symbols. Common values are default (default, all global symbols are visible) and hidden (default hidden, only symbols explicitly declared as visible are exported). By hiding unnecessary symbols, the size of the dynamic library’s symbol table can be reduced, loading time decreased, and symbol conflicts avoided.
Usage scenarios:
• Developing large dynamic libraries (e.g., device driver libraries, protocol stack libraries) that require control over external interfaces;• When multiple dynamic libraries coexist, avoid conflicts with symbols of the same name;• Need to reduce the size of the dynamic library by removing redundant symbol information.
Detailed example:
Taking a communication protocol library for an embedded device (protocol.so) as an example, we will compare the effects of default visibility and restricted visibility.
1. Default visibility (not using -fvisibility):
|
arm-linux-gnueabihf-gcc -fPIC -shared -o protocol.so protocol.c |
The generated protocol.so size is 150KB. Use the nm command to view the exported symbols:
|
arm-linux-gnueabihf-nm -D protocol.so | grep ” T “ 0000000000001234 T _init 0000000000001560 T send_data # External interface function 0000000000001680 T recv_data # External interface function 0000000000001820 T crc_calculate # Internal helper function (should be hidden) 0000000000001950 T buffer_check # Internal helper function (should be hidden) |
It can be seen that the internal helper functions crc_calculate and buffer_check are also exported, increasing the library size and conflict risk.
2. Restricted visibility (using -fvisibility=hidden):
|
# By default, symbols are hidden at compile time, only explicitly exporting necessary symbols arm-linux-gnueabihf-gcc -fPIC -shared -fvisibility=hidden -o protocol.so protocol.c |
In the code, it is necessary to explicitly declare external interfaces using __attribute__((visibility(“default”))):
|
// protocol.c // External interface: explicitly declared as visible __attribute__((visibility(“default”))) int send_data(const char* data, int len) { // Call internal function crc_calculate(data, len); // … } __attribute__((visibility(“default”))) int recv_data(char* buf, int max_len) { buffer_check(buf, max_len); // … } // Internal helper function: default hidden, not exported static int crc_calculate(const char* data, int len) { // … } int buffer_check(char* buf, int max_len) { // Not explicitly declared, default hidden // … } |
The generated protocol.so size is 120KB, a reduction of 20%. Use the nm command to view:
|
arm-linux-gnueabihf-nm -D protocol.so | grep ” T “ 0000000000001234 T _init 0000000000001560 T send_data # Only explicitly declared interfaces are exported 0000000000001680 T recv_data |
Internal functions crc_calculate and buffer_check are no longer exported, making the symbol table more concise.
Notes:
• Static functions are hidden by default and do not require additional handling; non-static internal functions need to rely on -fvisibility=hidden to hide;• Dependent functions between dynamic libraries must be explicitly exported, otherwise the caller will report an “undefined symbol” error;• Symbol export can be more finely controlled through version scripts (Version Script) (e.g., grouping by version), which works better in conjunction with -fvisibility.
Practical experience:
I once developed a collection of dynamic libraries containing multiple sensor drivers. Initially, due to not restricting symbol visibility, there was a conflict with the same name function parse_frame in two libraries. After switching to -fvisibility=hidden and explicitly exporting interfaces, the conflict was resolved, and the average size of each library was reduced by 15%, shortening the library loading time by 0.8 seconds during device startup.
The core of -fvisibility is the “principle of least privilege”—only expose necessary interfaces and hide internal implementations. This is especially important in the multi-library collaboration scenario of embedded Linux, as it optimizes resource usage and improves system stability. Tomorrow, we will discuss the -Wl,–stack option to control stack size and see how to avoid stack overflow issues in embedded programs.
-end-
If this article has helped you, please like, share, and follow. Thank you very much.