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

ABI và những thứ phá vỡ nó

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

  • Phân biệt API với ABI bằng ví dụ cụ thể
  • Chỉ ra chương trình chạy sai sau khi thêm một trường vào struct
  • Kể sáu thay đổi phá vỡ ABI
  • Kể ba thay đổi giữ được ABI

API là thứ trình biên dịch kiểm tra được. ABI là thứ không ai kiểm tra cho tới lúc chương trình chạy sai. Đó là toàn bộ lý do bài này tồn tại.

#API và ABI

API, giao diện mức mã nguồn
Tên hàm, danh sách tham số, kiểu trả về, tên trường. Nếu API tương thích thì mã nguồn cũ dịch lại được với thư viện mới.
ABI, giao diện mức nhị phân
Độ dời từng trường trong struct, kích thước struct, quy ước truyền tham số, cách trang trí tên ký hiệu. Nếu ABI tương thích thì tệp nhị phân cũ chạy được ngay với thư viện mới, không cần dịch lại.
Thay đổiAPI còn tương thíchABI còn tương thích
Thêm một hàm mớiCóCó
Thêm một trường vào CUỐI structCóKhông
Thêm một trường vào GIỮA structCóKhông
Đổi tên tham số trong nguyên mẫuCóCó
Đổi int thành long ở tham sốCó, thường thếKhông
Đổi thứ tự hai tham sốKhôngKhông
Đổi một macro hằng sốCóKhông
Sửa lỗi bên trong một hàmCóCó

#Thí nghiệm: thêm một trường

cauhinh.h, bản 1
#ifndef CAUHINH_H
#define CAUHINH_H

typedef struct {
    int rong;
    int cao;
} CauHinh;

void ch_in(const CauHinh *c);

#endif
app.c, không bao giờ thay đổi trong cả thí nghiệm
#include <stdio.h>
#include "cauhinh.h"

int main(void) {
    CauHinh c = { .rong = 1920, .cao = 1080 };

    printf("app  ghi : rong=%d cao=%d  (sizeof=%d)\n",
           c.rong, c.cao, (int)sizeof c);

    ch_in(&c);

    return 0;
}
terminal
# Cả hai cùng dịch với bản 1: đúng như mong đợi
gcc -fPIC -c cauhinh.c && gcc -shared cauhinh.o -o libch.dll -Wl,--out-implib,libch.dll.a && gcc app.c -L. -lch -o app.exe && ./app.exe
app  ghi : rong=1920 cao=1080  (sizeof=8)
thu vien doc: rong=1920 cao=1080
cauhinh.h, bản 2: thêm một trường vào giữa
typedef struct {
    int rong;
    int sau;      /* THEM VAO GIUA */
    int cao;
} CauHinh;
terminal
# Dịch lại CHỈ thư viện, app.exe giữ nguyên không đụng tới
gcc -fPIC -c cauhinh.c && gcc -shared cauhinh.o -o libch.dll -Wl,--out-implib,libch.dll.a && ./app.exe
app  ghi : rong=1920 cao=1080  (sizeof=8)
thu vien doc: rong=1920 cao=7742752

#Sáu thay đổi phá vỡ ABI

Một: đổi bố cục struct công khai

typedef struct { int a; int b; } S;            /* v1, sizeof 8 */
typedef struct { int a; int c; int b; } S;    /* v2: them, doi do doi */
typedef struct { long a; int b; } S;          /* v2: doi kieu, doi kich thuoc */
typedef struct { int b; int a; } S;           /* v2: doi thu tu */

/* Ca bon deu pha ABI. */

Hai: đổi chữ ký hàm

int  ham(int x);            /* v1 */
long ham(long x);           /* v2: pha ABI, va tren nhieu he thong
                               con khong pha API vi C tu chuyen kieu,
                               nen loi im lang hoan toan */

void f(int a, int b);       /* v1 */
void f(int b, int a);       /* v2: chi doi TEN tham so -> khong pha gi */
void f(int a, int b, int c);/* v2: them tham so -> pha ABI */

Ba: xóa hoặc đổi tên hàm công khai

/* v1 co ml_dien_tich, v2 doi thanh ml_tinh_dien_tich */

/* Chuong trinh cu tim ky hieu ml_dien_tich luc chay:
     ./app: symbol lookup error: ./app: undefined symbol: ml_dien_tich

   Day la loi duy nhat trong nhom nay ma he thong BAO RO,
   nen no de sua nhat. */

/* Neu buoc phai doi ten, giu ban cu lam mot lop bao mong: */
double ml_dien_tich(double r) { return ml_tinh_dien_tich(r); }

Bốn: đổi giá trị hằng số enum hoặc macro

typedef enum { MAU_DO, MAU_XANH, MAU_VANG } Mau;      /* 0 1 2 */
typedef enum { MAU_DO, MAU_TIM, MAU_XANH, MAU_VANG } Mau;  /* v2 */

/* Chuong trinh cu truyen 1 khi no nghi la MAU_XANH.
   Thu vien moi doc 1 va hieu la MAU_TIM.

   Cach dung: chi bao gio them vao CUOI: */
typedef enum { MAU_DO, MAU_XANH, MAU_VANG, MAU_TIM } Mau;

/* Voi macro thi giong het: */
#define ML_TOI_DA 100        /* v1, dong bang vao ma nguoi dung */
#define ML_TOI_DA 200        /* v2: nguoi dung cu van dung 100 */

Năm: đổi ý nghĩa mà không đổi chữ ký

/* v1: tra ve 0 khi thanh cong, -1 khi loi */
int ml_luu(const char *tep);

/* v2: tra ve so byte da ghi, hoac so am khi loi */
int ml_luu(const char *tep);

/* Chu ky giong het. Trinh bien dich khong noi gi. Nhung:

     if (ml_luu("a.txt") != 0) bao_loi();      <- gio bao loi khi THANH CONG

   Day la dang pha vo nguy hiem nhat, vi khong cong cu nao bat duoc.
   Neu ban phai doi ngu nghia thi hay DOI TEN HAM. */

Sáu: đổi cờ dịch ảnh hưởng ABI

gcc -m32 / -m64                /* 32 hay 64 bit */
gcc -fshort-enums              /* enum dung kieu nho nhat du chua */
gcc -fpack-struct              /* bo het phan dem */
gcc -mfloat-abi=hard / softfp  /* tren ARM: truyen so thuc bang thanh ghi nao */
gcc -std=c99 / -std=c11        /* it khi, nhung co the doi kich thuoc kieu */

/* Ban va nguoi dung thu vien PHAI dung cung nhung co nay.
   Do la ly do he thong goi nhu pkg-config ton tai: no luu lai
   dung bo co da dung de dich thu vien. Xem Bai 39.12. */

#Những thay đổi giữ được ABI

Phá ABI
/* v2 pha ABI */

typedef struct { int a, b, c; } S;   /* them c */

int ham(long x);                     /* doi kieu */

#define TOI_DA 200                   /* doi hang so */

typedef enum { A, MOI, B } E;        /* chen vao giua */
Giữ ABI
/* v2 giu ABI */

void ham_moi(void);                  /* THEM ham la an toan */

typedef struct { int a, b; } S;      /* khong dung toi */

int ham(int x) { /* sua ruot */ }    /* sua cai dat la an toan */

typedef enum { A, B, MOI } E;        /* them vao CUOI */

double ml_toi_da(void);              /* hang so thanh HAM */
Thay đổiAn toànGhi chú
Thêm một hàm mớiCóCách mở rộng an toàn nhất
Sửa lỗi bên trong một hàmCóMiễn là ngữ nghĩa không đổi
Thêm hằng số enum vào CUỐICóKhông bao giờ chèn vào giữa
Đổi tên tham số trong nguyên mẫuCóTên tham số không vào ABI
Thêm const vào tham số con trỏCóconst không đổi cách truyền
Đổi hàm thành static inline trong headerKhôngNgười dùng cũ vẫn tìm ký hiệu
Thêm trường vào struct MỜCóĐây là cả điểm của kiểu mờ, Bài 39.8

#Kiểm tra ABI tự động

# 1. abi-compliance-checker: so hai ban thu vien
abi-dumper libmath.so.1.0 -o v1.dump -lver 1.0
abi-dumper libmath.so.1.1 -o v2.dump -lver 1.1
abi-compliance-checker -l libmath -old v1.dump -new v2.dump

# Ket qua la mot bao cao HTML liet ke tung thay doi va muc do:
#   Binary compatibility: 87.5%
#   Removed Symbols: 1
#   Problems with Data Types: High severity: 2

# 2. abidiff, cua bo cong cu libabigail
abidiff libmath.so.1.0 libmath.so.1.1

# Functions changes summary: 0 Removed, 1 Changed, 2 Added
# Variables changes summary: 0 Removed, 0 Changed, 0 Added

# 3. Kiem tra thu cong bang nm, cach nhanh va tho
nm -D --defined-only libmath.so.1.0 | awk '{print $3}' | sort > v1.txt
nm -D --defined-only libmath.so.1.1 | awk '{print $3}' | sort > v2.txt
comm -23 v1.txt v2.txt        # ky hieu BI XOA -> pha ABI
comm -13 v1.txt v2.txt        # ky hieu THEM MOI -> an toan

Tự làm thử

  1. Chạy lại thí nghiệm thêm trường vào giữa và ghi lại giá trị rác bạn nhận được.
  2. Thử thêm trường vào cuối và chứng minh nó vẫn phá vỡ mọi thứ khi dùng mảng.
  3. Đổi int thành long ở một tham số và xem trình biên dịch có nói gì không.
  4. Chèn một hằng số enum vào giữa và quan sát chương trình cũ hiểu sai.
  5. Thêm ba dòng _Static_assert chốt bố cục rồi cố tình phá.
  6. Chạy abidiff giữa hai bản thư viện của bạn.

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

  • API là thứ trình biên dịch kiểm tra được, ABI thì không ai kiểm tra cho tới lúc chạy sai.
  • Thêm một trường vào giữa struct làm thư viện đọc ra 7742752 thay vì 1080, không sập và không cảnh báo.
  • Thêm vào cuối cũng không an toàn: kích thước đổi thì mảng, phép ghi và truyền theo giá trị đều hỏng.
  • Đổi ngữ nghĩa mà giữ nguyên chữ ký là dạng phá vỡ nguy hiểm nhất, vì không công cụ nào bắt được.
  • Rẻ nhất là chốt bố cục bằng _Static_assert; tốt nhất là đừng lộ struct ra ngoài.