Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 39.626 phút đọc

Điều khiển ký hiệu xuất ra

Sau bài này bạn sẽ làm được

  • Đếm số ký hiệu một thư viện xuất ra
  • Giấu bớt ký hiệu bằng visibility trên ELF
  • Dùng __declspec(dllexport) trên Windows
  • Nêu bốn lợi ích của việc giấu ký hiệu nội bộ

Mỗi hàm không khai báo static đều bị xuất ra khỏi thư viện của bạn, kể cả những hàm bạn viết cho riêng mình. Người dùng thư viện gọi được chúng, và từ lúc đó chúng thành một phần API mà bạn không định tạo ra.

#Mặc định xuất ra mọi thứ

src/noibo.c
#include "mathlib.h"

/* Ham nay chi de hai ham khac trong thu vien dung.
   No KHONG co trong mathlib.h. */
int ham_noi_bo_khong_muon_xuat(int x) { return x * 2; }

double ham_cong_khai(double r) { return dt_hinh_tron(r); }
terminal
gcc -shared build/*.o -o build/lib2.dll && objdump -p build/lib2.dll | sed -n '/Ordinal.Name Pointer. Table/,/^$/p'
[Ordinal/Name Pointer] Table
	[   0] cv_hinh_tron
	[   1] dt_hinh_tron
	[   2] giai_thua
	[   3] ham_cong_khai
	[   4] ham_noi_bo_khong_muon_xuat
	[   5] uoc_chung_lon_nhat

#Trên ELF: visibility

/* Bon muc hien thi, tu ro nhat toi kin nhat: */

__attribute__((visibility("default")))    /* xuat ra, mac dinh */
__attribute__((visibility("protected")))  /* xuat ra, nhung ben ngoai
                                             khong de len duoc */
__attribute__((visibility("hidden")))     /* KHONG xuat ra khoi .so,
                                             nhung cac .o trong .so dung duoc */
__attribute__((visibility("internal")))   /* nhu hidden, va them mot vai
                                             bao dam ve ABI */
Cách dùng: giấu hết rồi mở ra từng cái
# Dich TOAN BO thu vien voi mac dinh la an
gcc -fvisibility=hidden -fPIC -c src/*.c
gcc -shared *.o -o libmath.so

# Roi danh dau nhung ham CONG KHAI trong header:

/* mathlib.h */
#define ML_API __attribute__((visibility("default")))

ML_API double dt_hinh_tron(double r);
ML_API double cv_hinh_tron(double r);
ML_API long   giai_thua(int n);

/* Ham noi bo thi khong danh dau gi, nen no theo mac dinh: an. */
terminal
# Trên Linux, đo số ký hiệu trước và sau
nm -D --defined-only libmath.so | grep ' T ' | wc -l
6
gcc -fvisibility=hidden -fPIC -c src/*.c && gcc -shared *.o -o libmath.so && nm -D --defined-only libmath.so | grep ' T ' | wc -l
3

#Trên Windows: dllexport

terminal
# -fvisibility=hidden KHÔNG có tác dụng trên PE
gcc -shared -fvisibility=hidden build/*.o -o build/lib4.dll && objdump -p build/lib4.dll | sed -n '/Ordinal.Name Pointer. Table/,/^$/p'
[Ordinal/Name Pointer] Table
	[   0] cv_hinh_tron
	[   1] dt_hinh_tron
	[   2] giai_thua
	[   3] ham_cong_khai
	[   4] ham_noi_bo_khong_muon_xuat
	[   5] uoc_chung_lon_nhat
src/noibo2.c
#include "mathlib.h"

int ham_noi_bo_khong_muon_xuat(int x) { return x * 2; }

__declspec(dllexport)
double ham_cong_khai(double r) { return dt_hinh_tron(r); }
terminal
gcc -shared build/hinhhoc_pic.o build/sohoc_pic.o build/noibo2.o -o build/lib3.dll && objdump -p build/lib3.dll | sed -n '/Ordinal.Name Pointer. Table/,/^$/p'
[Ordinal/Name Pointer] Table
	[   0] ham_cong_khai
ELF, tức Linux và macOSPE, tức Windows
Mặc địnhXuất ra tất cảXuất ra tất cả
Cách giấu-fvisibility=hidden, rồi mở từng cáiĐánh dấu dllexport cho cái muốn xuất
Cần đánh dấu khi nhậpKhôngNên có dllimport, để tối ưu
Ký hiệu bị đè lúc chạy đượcCó, đó là interpositionKhông
Kịch bản phiên bảnCóKhông, nhưng có tệp .def
Kiểu đánh dấuThuộc tính GCCTừ khóa mở rộng của MSVC
Tệp .def, cách của Windows tương đương kịch bản phiên bản
; libmath.def
LIBRARY libmath
EXPORTS
    dt_hinh_tron
    cv_hinh_tron
    giai_thua
    uoc_chung_lon_nhat

; Dich:
;   gcc -shared build/*.o libmath.def -o libmath.dll
;
; Uu diem so voi dllexport: danh sach nam o MOT cho, doc duoc,
; va sua duoc ma khong dung toi ma nguon.
;
; Va no cho phep dat SO THU TU co dinh cho tung ham:
;    dt_hinh_tron @1
;    cv_hinh_tron @2
; De giu ABI on dinh khi ban them ham moi.

#Một macro cho cả hai

include/mathlib_api.h
#ifndef MATHLIB_API_H
#define MATHLIB_API_H

/* Ba truong hop:
     MATHLIB_BUILD    dang DICH chinh thu vien   -> xuat ra
     MATHLIB_STATIC   dung ban tinh              -> khong danh dau gi
     mac dinh         dang DUNG thu vien dong    -> nhap vao */

#if defined(_WIN32) || defined(__CYGWIN__)
  #if defined(MATHLIB_STATIC)
    #define ML_API
  #elif defined(MATHLIB_BUILD)
    #define ML_API __declspec(dllexport)
  #else
    #define ML_API __declspec(dllimport)
  #endif
  #define ML_NOI_BO
#else
  #if defined(MATHLIB_STATIC)
    #define ML_API
  #else
    #define ML_API    __attribute__((visibility("default")))
  #endif
  #define ML_NOI_BO   __attribute__((visibility("hidden")))
#endif

#endif
include/mathlib.h
#ifndef MATHLIB_H
#define MATHLIB_H

#include "mathlib_api.h"

#ifdef __cplusplus
extern "C" {
#endif

ML_API double dt_hinh_tron(double r);
ML_API double cv_hinh_tron(double r);
ML_API long   giai_thua(int n);
ML_API int    uoc_chung_lon_nhat(int a, int b);

#ifdef __cplusplus
}
#endif

#endif
# Dich thu vien dong
gcc -DMATHLIB_BUILD -fvisibility=hidden -fPIC -c -Iinclude src/*.c
gcc -shared *.o -o libmath.so

# Dich thu vien tinh
gcc -DMATHLIB_STATIC -c -Iinclude src/*.c
ar rcs libmath.a *.o

# Nguoi dung ban dong: khong dinh nghia gi
gcc app.c -Iinclude -L. -lmath -o app

# Nguoi dung ban tinh
gcc -DMATHLIB_STATIC app.c -Iinclude -L. -l:libmath.a -o app

#Bốn lợi ích

Xuất ra mọi thứ
$ nm -D --defined-only libfoo.so | wc -l
12043

/* Nguoi dung thay 12043 ky hieu va khong biet cai nao la API.
   Tai lieu noi 340 ham, con lai la gi?

   Va ban khong dam doi ten mot ham noi bo nao, vi biet dau
   co nguoi dang goi no. */
Chỉ xuất ra API
$ nm -D --defined-only libfoo.so | wc -l
340

/* Dung bang so ham trong tai lieu.

   Ban doi ten, gop, tach, xoa bat cu ham noi bo nao ma khong
   pha vo gi ca. Ranh gioi giua API va cai dat la ro rang
   va duoc trinh lien ket thuc thi. */
Lợi íchMức độ
API rõ ràng, đúng bằng tài liệuĐây là lợi ích lớn nhất, và nó là lợi ích thiết kế chứ không phải kỹ thuật
Thư viện nhỏ hơnMười tới hai mươi phần trăm với thư viện nhiều ký hiệu
Nạp nhanh hơnĐáng kể khi có hàng nghìn ký hiệu
Tối ưu tốt hơnHàm hidden nội tuyến được và không qua PLT
Tránh xung đột tênHai thư viện cùng có hàm tên log_init không đụng nhau nữa

#Kịch bản phiên bản

libmath.map
MATHLIB_1.0 {
  global:
    dt_hinh_tron;
    cv_hinh_tron;
    giai_thua;
    uoc_chung_lon_nhat;
  local:
    *;                    # moi thu con lai deu an
};

MATHLIB_1.1 {
  global:
    ml_dt_elip;           # ham them o ban 1.1
} MATHLIB_1.0;            # ke thua tu 1.0
gcc -shared *.o -Wl,--version-script=libmath.map -o libmath.so

# Ba uu diem so voi visibility:
#
#   1. Danh sach API nam o MOT tep, doc duoc, dua vao kho ma duoc
#   2. Khong phai sua ma nguon, nen dung duoc voi thu vien ben thu ba
#   3. Ho tro DANH PHIEN BAN cho tung ky hieu: mot .so co the chua
#      HAI ban cua cung mot ham, cho ca chuong trinh cu lan moi

$ nm -D libmath.so | grep dt_hinh_tron
0000000000001110 T dt_hinh_tron@@MATHLIB_1.0

# Hai dau a cong nghia la ban MAC DINH khi lien ket moi.
# Mot dau a cong la ban cu, giu lai cho chuong trinh da lien ket truoc.

Tự làm thử

  1. Đếm số ký hiệu thư viện của bạn xuất ra và so với số hàm trong header.
  2. Bật -fvisibility=hidden trên Linux và đếm lại.
  3. Xác nhận -fvisibility=hidden không có tác dụng trên MinGW.
  4. Đánh dấu một hàm bằng __declspec(dllexport) và xem năm hàm kia biến mất.
  5. Viết macro ML_API chạy được trên cả ba nền tảng.
  6. Viết một kịch bản phiên bản và kiểm tra bằng nm -D.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Mặc định thư viện xuất ra mọi ký hiệu không static, kể cả những cái không có trong header.
  • Trên ELF dùng -fvisibility=hidden rồi mở từng hàm; trên PE đánh dấu dllexport, và chỉ cần một hàm được đánh dấu là năm hàm kia biến mất.
  • Giấu ký hiệu vừa giảm kích thước vừa tăng tốc, vì hàm hidden nội tuyến được và không phải đi qua PLT.
  • Xung đột tên giữa hai thư viện là lỗi khó nhất mà việc giấu ký hiệu xóa bỏ hoàn toàn.
  • Kịch bản phiên bản của ELF cho phép một tệp .so chứa nhiều bản của cùng một hàm, và đó là cách glibc giữ tương thích ngược.