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.
#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
#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;
}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 variableKhi 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ợp | strncpy làm gì | Kết quả |
|---|---|---|
| Nguồn ngắn hơn n | Chép nguồn rồi điền byte 0 vào TOÀN BỘ phần còn lại | Chuỗi hợp lệ, nhưng tốn thời gian với bộ đệm lớn |
| Nguồn dài đúng n | Chép n byte, KHÔNG thêm byte kết thúc | Không phải chuỗi hợp lệ |
| Nguồn dài hơn n | Chép n byte đầu, KHÔNG thêm byte kết thúc | Không phải chuỗi hợp lệ |
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ểm | Chi tiết |
|---|---|
| Thuộc chuẩn C99 | Có ở mọi nơi, không cần thư viện phụ |
| Luôn thêm byte kết thúc | Kể cả khi chuỗi bị cắt, miễn là co lớn hơn 0 |
| Trả về độ dài đáng lẽ cần | So với co là biết ngay có bị cắt hay không |
| Ghép nhiều thứ cùng lúc | Không chỉ chép, mà còn định dạng |
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àm | Nhận kích thước đích | Luôn kết thúc chuỗi | Báo bị cắt |
|---|---|---|---|
| strcpy | Không | Có | Không |
| strncpy | Có | Không | Không |
| strlcpy | Có | Có | Có, nhưng không thuộc chuẩn |
| snprintf | Có | Có | Có |
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
/* 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
/* 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;
}#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ử
- 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. - Dùng
strncpyvớ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. - Viết hàm
chep_an_toandùngsnprintf, thử với chuỗi vừa, chuỗi bị cắt và bộ đệm kích thước 0. - Cài
my_strcpytheo cả hai cách trong bài, so kết quả với hàm chuẩn trên nhiều dữ liệu. - Cài
my_strlcpyvà chạy đủ bộ kiểm thử, đặc biệt trường hợpcobằng 0. - 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
strcpykhô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.strncpykhông phải bản an toàn củastrcpy. Khi nguồn dài bằng hoặc hơn n, nó không thêm byte kết thúc.snprintflà 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 nganggets. - Hàm tự viết phải xử lý trường hợp kích thước bộ đệm bằng 0, vì
co - 1khi đó cho ra số khổng lồ.