Bài 11.218 phút đọc
Ký tự kết thúc chuỗi
Sau bài này bạn sẽ làm được
- Giải thích vai trò của ký tự null trong mọi hàm chuỗi
- Phân biệt kích thước mảng, độ dài chuỗi và số ô đã dùng
- Nhận ra khi nào một thao tác làm mất ký tự kết thúc
- Dùng công cụ để phát hiện đọc vượt vùng nhớ
Một byte duy nhất quyết định toàn bộ hành vi của thư viện chuỗi. Bài này nói kỹ về nó: nó ở đâu, khi nào nó biến mất, và chuyện gì xảy ra khi nó không còn.
#Vai trò của byte 0
Ký tự kết thúc chuỗi
Byte có giá trị 0, viết trong mã nguồn là
'\0'. Nó không phải ký tự số 0, mà là giá trị 0. Ký tự số 0 có mã ASCII là 48.'\0' /* giá trị 0, ký tự kết thúc chuỗi */
'0' /* giá trị 48, ký tự chữ số không */
0 /* giá trị 0, tương đương '\0' nhưng viết vậy dễ nhầm với NULL */
"0" /* chuỗi hai byte: 48 rồi 0 */Mọi hàm trong string.h đều hoạt động theo cùng một cách: đi từng byte cho tới khi gặp giá trị 0. Không hàm nào biết mảng của bạn dài bao nhiêu, và không hàm nào kiểm tra biên.
/* Đây gần như là toàn bộ cài đặt của strlen */
size_t strlen(const char *s)
{
const char *p = s;
while (*p != '\0')
++p;
return (size_t)(p - s);
}#Ba con số dễ nhầm
ba-con-so.c
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[10] = "hello";
printf("sizeof s = %zu\n", sizeof s); /* 10, kích thước mảng */
printf("strlen(s) = %zu\n", strlen(s)); /* 5, số ký tự trước byte 0 */
printf("da dung = %zu\n", strlen(s) + 1); /* 6, số ô đang có ý nghĩa */
return 0;
}terminal
./ba-con-so
sizeof s = 10 strlen(s) = 5 da dung = 6
| Con số | Với char s[10] = "hello" | Biết được lúc nào |
|---|---|---|
| Kích thước mảng | 10 | Lúc biên dịch, và chỉ ở nơi khai báo |
| Độ dài chuỗi | 5 | Lúc chạy, phải đếm nên tốn thời gian |
| Số ô đã dùng | 6 | Bằng độ dài cộng một |
| Chỗ trống còn lại | 4 | Bằng kích thước trừ độ dài trừ một |
#Khi nào ký tự kết thúc bị mất
Trường hợp 1: khai báo vừa khít
char s[5] = "hello"; /* 5 ký tự vào 5 ô, không còn chỗ cho byte 0 */Trường hợp 2: strncpy khi nguồn quá dài
char dich[5];
strncpy(dich, "hello world", 5); /* chép đúng 5 byte, KHÔNG thêm byte 0 */
printf("%s\n", dich); /* đọc tràn */Trường hợp 3: điền mảng bằng vòng lặp
char s[6];
for (int i = 0; i < 5; ++i)
s[i] = 'a' + i;
/* Quên s[5] = '\0'; nên s không phải chuỗi hợp lệ */Trường hợp 4: memcpy nhầm số byte
char nguon[] = "hello";
char dich[10];
memcpy(dich, nguon, strlen(nguon)); /* chép 5 byte, thiếu byte kết thúc */
memcpy(dich, nguon, strlen(nguon) + 1); /* đúng: chép 6 byte */
memcpy(dich, nguon, sizeof nguon); /* cũng đúng: sizeof đã tính cả byte 0 */Trường hợp 5: đọc dữ liệu nhị phân
char bo_dem[256];
size_t n = fread(bo_dem, 1, sizeof bo_dem - 1, f);
bo_dem[n] = '\0'; /* BẮT BUỘC: fread không thêm byte kết thúc */#Hậu quả khi thiếu
thieu.c
#include <stdio.h>
#include <string.h>
int main(void)
{
char a[5] = "hello"; /* không có byte kết thúc */
char b[5] = "world"; /* nằm ngay cạnh trong bộ nhớ */
printf("[%s]\n", a);
printf("strlen(a) = %zu\n", strlen(a));
return 0;
}terminal
gcc -std=c17 -Wall thieu.c -o t && ./t
[helloworld] strlen(a) = 10
# Kết quả này hoàn toàn ngẫu nhiên, phụ thuộc cách trình biên dịch xếp biến
gcc -O2 thieu.c -o t && ./t
[hello\xc0\x8e...] strlen(a) = 23
| Hậu quả | Mức nghiêm trọng | Giải thích |
|---|---|---|
| In ra rác | Nhẹ | Chỉ khó chịu, dễ phát hiện |
| strlen trả về số quá lớn | Trung bình | Mọi tính toán dựa trên nó đều sai |
| Rò rỉ dữ liệu nhạy cảm | Nặng | Nội dung bộ nhớ kế bên bị in ra hoặc gửi đi |
| Chương trình sập | Nặng | Đọc tới vùng nhớ không thuộc tiến trình |
| Ghi tràn khi dùng làm nguồn để chép | Rất nặng | Là lỗ hổng bảo mật khai thác được |
#Tự viết hàm dừng đúng chỗ
tu-viet.c
/* Mẫu chuẩn: duyệt tới khi gặp byte 0 */
size_t dem_ky_tu(const char *s, char c)
{
size_t dem = 0;
for (size_t i = 0; s[i] != '\0'; ++i)
if (s[i] == c)
++dem;
return dem;
}
/* Mẫu bằng con trỏ, cách viết của thư viện chuẩn */
size_t dem_ky_tu2(const char *s, char c)
{
size_t dem = 0;
while (*s != '\0') {
if (*s == c) ++dem;
++s;
}
return dem;
}Hàm nhận độ dài từ bên ngoài mà không kiểm tra
void chep(char *dich, const char *nguon)
{
size_t i = 0;
while (nguon[i] != '\0') {
dich[i] = nguon[i]; /* không biết dich còn bao nhiêu chỗ */
++i;
}
dich[i] = '\0';
}Hàm luôn nhận cả kích thước bộ đệm
/* Trả về độ dài chuỗi nguồn để người gọi biết có bị cắt không. */
size_t chep(char *dich, size_t co, const char *nguon)
{
size_t i = 0;
if (co == 0) return strlen(nguon);
while (nguon[i] != '\0' && i + 1 < co) {
dich[i] = nguon[i];
++i;
}
dich[i] = '\0';
return strlen(nguon);
}Quy ước trả về độ dài chuỗi nguồn là quy ước của hàm strlcpy trên các hệ BSD và snprintf trong chuẩn C. Người gọi so kết quả với kích thước bộ đệm để biết chuỗi có bị cắt hay không.
char dich[8];
if (chep(dich, sizeof dich, nguon) >= sizeof dich)
fprintf(stderr, "Canh bao: chuoi bi cat\n");Tự làm thử
- In giá trị số của
'\0','0'và"0"[0], giải thích ba con số. - Khai báo
char s[10] = "hello"rồi in cảsizeof,strlenvà chỗ trống còn lại. - Tạo hai mảng năm ký tự cạnh nhau, cả hai đều thiếu byte kết thúc, in ra và quan sát hiện tượng nối liền.
- Dùng
strncpyvới chuỗi nguồn dài hơn bộ đệm, in kết quả, rồi sửa bằng cách tự đặt byte 0 và bằngsnprintf. - Dùng
memcpychép chuỗi với ba giá trị độ dài khác nhau như trong bài, quan sát cái nào đúng. - Cài hàm
chepcó kiểm tra kích thước, thử với bộ đệm quá nhỏ và xác nhận nó cắt chuỗi đúng cách.
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
- Ký tự kết thúc là byte giá trị 0, viết là
'\0', khác hoàn toàn với ký tự chữ số'0'có mã 48. - Ba con số khác nhau: kích thước mảng, độ dài chuỗi, và số ô đã dùng bằng độ dài cộng một.
- Mọi hàm không có chữ
strtrong tên đều không tự thêm byte kết thúc.strncpycũng không, khi nguồn quá dài. - Thiếu byte kết thúc dẫn tới đọc tràn, và trong trường hợp xấu nhất là rò rỉ dữ liệu nhạy cảm.
- Hàm tự viết ghi vào bộ đệm luôn phải nhận thêm kích thước bộ đệm đó.