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

strcpy

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

  • Cài và dùng strcpy đúng
  • Giải thích vì sao strcpy là nguồn lỗi tràn bộ đệm
  • Dùng snprintf làm phương án an toàn
  • Biết cạm bẫy của strncpy khi chuỗi nguồn quá dài

Sao chép chuỗi là thao tác gây ra nhiều lỗ hổng bảo mật hơn bất kỳ thao tác nào khác trong C. Bài này trình bày ba hàm làm cùng một việc, chỉ ra vì sao hai hàm đầu khó dùng đúng, và vì sao hàm thứ ba nên là lựa chọn mặc định của bạn.

#Cách dùng

#include <string.h>

char *strcpy(char *dich, const char *nguon);

Chép toàn bộ nguon kể cả byte kết thúc vào dich, rồi trả về dich. Hàm này không nhận kích thước bộ đệm đích.

strcpy.c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char dich[20];

    strcpy(dich, "hello");
    printf("[%s]\n", dich);         /* [hello] */

    strcpy(dich, "xin chao");
    printf("[%s]\n", dich);         /* [xin chao], ghi đè hoàn toàn */

    return 0;
}

#Vì sao strcpy nguy hiểm

tran.c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char nho[8];
    char canh[16] = "GIU NGUYEN";  /* biến kế bên */

    strcpy(nho, "mot chuoi rat dai vuot qua tam ô");

    printf("nho  = [%s]\n", nho);
    printf("canh = [%s]\n", canh);

    return 0;
}
terminal
gcc -std=c17 -g -fsanitize=address tran.c -o t && ./t
ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
WRITE of size 33 at 0x7ffc... thread T0
    #0 in __asan_memcpy
    #1 in main tran.c:9

Address is located in stack of thread T0 at offset 40 in frame
  This frame has 2 object(s):
    [32, 40) 'nho' (line 6) <== Memory access overflows this variable

Khi nào strcpy dùng được

/* Chỉ khi bạn CHỨNG MINH được đích đủ chỗ, ngay tại chỗ gọi. */
char dich[64];

if (strlen(nguon) + 1 <= sizeof dich)
    strcpy(dich, nguon);
else
    fprintf(stderr, "Chuoi qua dai\n");

Cách này đúng, nhưng nếu bạn đã phải viết ba dòng thì snprintf một dòng vẫn tốt hơn. Trong mã mới, gần như không có lý do nào để gọi strcpy trực tiếp.

#strncpy không phải bản an toàn

char *strncpy(char *dich, const char *nguon, size_t n);
Trường hợpstrncpy làm gìKết quả
Nguồn ngắn hơn nChép nguồn rồi điền byte 0 vào TOÀN BỘ phần còn lạiChuỗi hợp lệ, nhưng tốn thời gian với bộ đệm lớn
Nguồn dài đúng nChép n byte, KHÔNG thêm byte kết thúcKhông phải chuỗi hợp lệ
Nguồn dài hơn nChép n byte đầu, KHÔNG thêm byte kết thúcKhông phải chuỗi hợp lệ
strncpy.c
char dich[8];

strncpy(dich, "hello", sizeof dich);
/* dich = h e l l o \0 \0 \0   -> hợp lệ, nhưng đã ghi 3 byte 0 thừa */

strncpy(dich, "mot chuoi dai", sizeof dich);
/* dich = m o t   c h u o    -> KHÔNG có byte kết thúc */

printf("%s\n", dich);      /* đọc tràn */

strlcpy trên các hệ BSD

size_t strlcpy(char *dich, const char *nguon, size_t co);

Hàm này làm đúng điều bạn muốn: chép nhiều nhất co - 1 ký tự, luôn thêm byte kết thúc, và trả về độ dài chuỗi nguồn để bạn biết có bị cắt hay không. Nhược điểm là nó không thuộc chuẩn C, có sẵn trên macOS và BSD, cần thư viện phụ trên Linux.

if (strlcpy(dich, nguon, sizeof dich) >= sizeof dich)
    fprintf(stderr, "Chuoi bi cat\n");

#snprintf, phương án nên dùng

#include <stdio.h>

int snprintf(char *dich, size_t co, const char *dinh_dang, ...);
Ưu điểmChi tiết
Thuộc chuẩn C99Có ở mọi nơi, không cần thư viện phụ
Luôn thêm byte kết thúcKể cả khi chuỗi bị cắt, miễn là co lớn hơn 0
Trả về độ dài đáng lẽ cầnSo với co là biết ngay có bị cắt hay không
Ghép nhiều thứ cùng lúcKhông chỉ chép, mà còn định dạng
snprintf.c
char dich[8];

int n = snprintf(dich, sizeof dich, "%s", "mot chuoi dai");

printf("[%s]\n", dich);      /* [mot chu], luôn có byte kết thúc */
printf("can %d ky tu\n", n); /* 13, độ dài đáng lẽ cần */

if (n < 0 || (size_t)n >= sizeof dich)
    fprintf(stderr, "Chuoi bi cat\n");

Ghép chuỗi bằng snprintf

char duong_dan[256];

snprintf(duong_dan, sizeof duong_dan, "%s/%s.%s", thu_muc, ten, duoi);

/* Một lời gọi thay cho một chuỗi strcpy và strcat, và an toàn hơn hẳn. */
HàmNhận kích thước đíchLuôn kết thúc chuỗiBáo bị cắt
strcpyKhôngKhông
strncpyKhôngKhông
strlcpyCó, nhưng không thuộc chuẩn
snprintf

Nhìn bảng này là thấy ngay vì sao snprintf nên là lựa chọn mặc định. Nó là hàm duy nhất trong chuẩn C thỏa cả ba tiêu chí.

#Tự cài lại

my-strcpy.c
/* Bản gốc: đơn giản nhưng không an toàn, đúng như hàm chuẩn. */
char *my_strcpy(char *dich, const char *nguon)
{
    char *d = dich;

    while ((*d++ = *nguon++) != '\0')
        ;

    return dich;
}

Bản an toàn tự viết

my-strlcpy.c
/* Chép nhiều nhất co - 1 ký tự, luôn kết thúc chuỗi.
   Trả về độ dài chuỗi nguồn, giống strlcpy của BSD. */
size_t my_strlcpy(char *dich, const char *nguon, size_t co)
{
    size_t n = strlen(nguon);

    if (co > 0) {
        size_t chep = n < co - 1 ? n : co - 1;

        memcpy(dich, nguon, chep);
        dich[chep] = '\0';
    }

    return n;
}
test.c
#include <assert.h>
#include <string.h>

int main(void)
{
    char d[8];

    assert(my_strlcpy(d, "abc", sizeof d) == 3);
    assert(strcmp(d, "abc") == 0);

    assert(my_strlcpy(d, "1234567", sizeof d) == 7);   /* vừa khít */
    assert(strcmp(d, "1234567") == 0);

    assert(my_strlcpy(d, "12345678", sizeof d) == 8);  /* bị cắt */
    assert(strcmp(d, "1234567") == 0);

    assert(my_strlcpy(d, "", sizeof d) == 0);
    assert(d[0] == '\0');

    assert(my_strlcpy(d, "abc", 0) == 3);              /* co bằng 0, không ghi gì */

    puts("Tat ca kiem thu deu qua");

    return 0;
}

Tự làm thử

  1. Tái hiện lỗi tràn bộ đệm bằng strcpy, quan sát biến kế bên bị đè, rồi chạy dưới -fsanitize=address.
  2. Dùng strncpy với chuỗi nguồn dài hơn bộ đệm và in kết quả bằng %s. Giải thích hiện tượng.
  3. Viết hàm chep_an_toan dùng snprintf, thử với chuỗi vừa, chuỗi bị cắt và bộ đệm kích thước 0.
  4. Cài my_strcpy theo cả hai cách trong bài, so kết quả với hàm chuẩn trên nhiều dữ liệu.
  5. Cài my_strlcpy và chạy đủ bộ kiểm thử, đặc biệt trường hợp co bằng 0.
  6. Ghép đường dẫn file từ ba phần bằng snprintf, và kiểm tra trường hợp kết quả dài hơn bộ đệm.

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

  • strcpy không nhận kích thước đích nên không thể dùng an toàn trừ khi bạn tự kiểm tra trước.
  • strncpy không phải bản an toàn của strcpy. Khi nguồn dài bằng hoặc hơn n, nó không thêm byte kết thúc.
  • snprintf là lựa chọn mặc định: thuộc chuẩn, luôn kết thúc chuỗi, và cho biết chuỗi có bị cắt hay không.
  • Không bao giờ dùng sprintf. Nó nguy hiểm ngang gets.
  • Hàm tự viết phải xử lý trường hợp kích thước bộ đệm bằng 0, vì co - 1 khi đó cho ra số khổng lồ.