Phần 1: Ẩn dữ liệu con trỏ trong c
Khi chúng ta viết mã C, con trỏ ở khắp mọi nơi. Chúng ta có thể sử dụng thêm một chút con trỏ và lén đưa thêm một số thông tin vào đó. Để thực hiện thủ thuật này, chúng ta khai thác sự căn chỉnh tự nhiên của dữ liệu trong bộ nhớ.
Dữ liệu trong bộ nhớ không được lưu trữ tại bất kỳ địa chỉ tùy ý nào. Bộ xử lý luôn đọc bộ nhớ theo từng khối có cùng kích thước với kích thước từ của nó; và do đó, vì lý do hiệu quả, trình biên dịch gán địa chỉ cho các thực thể trong bộ nhớ theo bội số của kích thước của chúng theo byte. Do đó, trên bộ xử lý 32 bit, một int 4 byte chắc chắn sẽ nằm ở một địa chỉ bộ nhớ chia hết cho 4.
Ở đây, tôi sẽ giả sử một hệ thống trong đó kích thước của int và kích thước của con trỏ là 4 byte.
Bây giờ chúng ta hãy xem xét một con trỏ đến một int. Như đã nói ở trên, int có thể được định vị tại địa chỉ bộ nhớ 0x1000 hoặc 0x1004 hoặc 0x1008, nhưng không bao giờ ở 0x1001 hoặc 0x1002 hoặc 0x1003 hoặc bất kỳ địa chỉ nào khác không chia hết cho 4.
Bây giờ, bất kỳ số nhị phân nào là bội số của 4 sẽ kết thúc bằng 00.
Về cơ bản, điều này có nghĩa là đối với bất kỳ con trỏ nào đến một int, 2 bit bậc thấp hơn của nó luôn bằng 0.
Bây giờ chúng ta có 2 bit không giao tiếp gì cả. Thủ thuật ở đây là đưa dữ liệu của chúng ta vào 2 bit này, sử dụng chúng bất cứ khi nào chúng ta muốn và sau đó xóa chúng trước khi chúng ta thực hiện bất kỳ truy cập bộ nhớ nào bằng cách hủy tham chiếu con trỏ.
Vì các phép toán bitwise trên con trỏ không phù hợp với tiêu chuẩn C, chúng ta sẽ lưu trữ con trỏ dưới dạng int không dấu.
Sau đây là một đoạn mã đơn giản để ngắn gọn. Xem kho lưu trữ github - hide-data-in-ptr để biết mã đầy đủ.
void put_data(int *p, unsigned int data)
{
assert(data < 4);
*p |= data;
}
unsigned int get_data(unsigned int p)
{
return (p & 3);
}
void cleanse_pointer(int *p)
{
*p &= ~3;
}
int main(void)
{ unsigned int x = 701;
unsigned int p = (unsigned int) &x;
printf("Original ptr: %u\n", p);
put_data(&p, 3);
printf("ptr with data: %u\n", p);
printf("data stored in ptr: %u\n", get_data(p));
cleanse_pointer(&p);
printf("Cleansed ptr: %u\n", p);
printf("Dereferencing cleansed ptr: %u\n", *(int*)p);
return 0;
}
Điều này sẽ đưa ra kết quả sau:
Original ptr: 3216722220
ptr with data: 3216722223
data stored in ptr: 3
Cleansed ptr: 3216722220
Dereferencing cleansed ptr: 701
Chúng ta có thể lưu trữ bất kỳ số nào có thể được biểu diễn bằng 2 bit trong con trỏ. Sử dụng put_data(), 2 bit cuối cùng của con trỏ được đặt làm dữ liệu cần lưu trữ. Dữ liệu này được truy cập bằng get_data(). Tại đây, tất cả các bit ngoại trừ 2 bit cuối cùng được ghi đè thành số không tại đó bằng cách tiết lộ dữ liệu ẩn của chúng ta.
cleanse_pointer() xóa 2 bit cuối cùng thành số không, khiến con trỏ an toàn khi hủy tham chiếu. Lưu ý rằng trong khi một số CPU như Intel sẽ cho phép chúng ta truy cập các vị trí bộ nhớ không được căn chỉnh, thì một số CPU khác như CPU ARM sẽ bị lỗi. Vì vậy, hãy luôn nhớ giữ cho con trỏ trỏ đến một vị trí được căn chỉnh trước khi hủy tham chiếu.
Điều này có được sử dụng ở bất kỳ đâu trong thế giới thực không?
Có, có. Xem triển khai Red Black Trees trong hạt nhân Linux (liên kết).
Nút của cây được định nghĩa bằng cách sử dụng:
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
Ở đây unsigned long __rb_parent_color lưu trữ:
1. địa chỉ của nút cha
2. màu của nút.
Màu được biểu diễn là 0 cho Đỏ và 1 cho Đen.
Giống như trong ví dụ trước, dữ liệu này được lén đưa vào các bit 'vô dụng' của con trỏ cha.
Bây giờ hãy xem, con trỏ cha và thông tin màu được truy cập như thế nào:
/* in rbtree.h */
#define rb_parent(r) ((struct rb_node *)((r)->__rb_parent_color & ~3))
/* in rbtree_augmented.h */
#define __rb_color(pc) ((pc) & 1)
#define rb_color(rb) __rb_color((rb)->__rb_parent_color)
Phần 2: Ký tự theo nghĩa đen không phải là một ký tự trong C!
Tôi đã từng viết một chương trình tương tự như sau. Nhưng tôi đã mắc một lỗi nhỏ dẫn tôi đến một điều thú vị.
#include <stdio.h>
int main()
{
char ch[256];
scanf("%s", ch);
if (ch == 'a') {
printf("Your sentence begins with %c.\n", *ch);
}
return 0;
}
Trong đoạn mã này, tôi được cho là đang đọc một chuỗi từ stdin, kiểm tra xem nó có bắt đầu bằng ký tự 'a' không và nếu có, hãy in ra một cái gì đó.
Tuy nhiên, nếu bạn xem đủ kỹ, tôi đã bỏ lỡ một toán tử hủy tham chiếu (*) ở dòng 6.
Đáng lẽ phải là if (*ch == 'a') {
Không nhận thấy lỗi, tôi đã tiếp tục biên dịch mã.
GCC đã đưa ra cảnh báo sau:
cảnh báo: so sánh giữa con trỏ và số nguyên
$ gcc -o test test.c
test.c:6:10: warning: comparison between pointer and integer ('char *' and 'int')
if (ch == 'a') {
~~ ^ ~~~
1 warning generated.
Bây giờ tôi biết phải sửa ở đâu, nhưng cảnh báo đã thu hút sự chú ý của tôi. Nó nói rằng tôi đang cố so sánh một con trỏ và một số nguyên. Nhưng số nguyên đến từ đâu?
Tôi không sử dụng số nguyên ở bất kỳ đâu trong mã ngoại trừ giá trị trả về của main(). Trong dòng đưa ra cảnh báo, tôi đang so sánh một con trỏ (ch) với một ký tự theo nghĩa đen ('a').
Sau một chút điều tra, tôi đã tìm ra lý do.
Trước khi tôi cho bạn biết lý do, hãy để tôi thử tìm kích thước của ký tự theo nghĩa đen và so sánh nó với kích thước của một int và char.
#include
int main()
{
printf("sizeof(char) %zu\n", sizeof(char));
printf("sizeof(int) %zu\n", sizeof(int));
printf("sizeof('a') %zu\n", sizeof('a'));
return 0;
}
Sau đây là kết quả:
sizeof(char) 1
sizeof(int) 4
sizeof('a') 4
Như bạn có thể thấy, ký tự theo nghĩa đen không chiếm kích thước của char, mà chiếm kích thước của int.
Đây là những gì tiêu chuẩn C nói (§6.4.4.4):
883 An integer character constant has type int.
...
886 If an integer character constant contains a single
character or escape sequence, its value is the one that
results when an object with type char whose value is that of
the single character or escape sequence is converted to
type int.
Có. Trong C, một ký tự theo nghĩa đen là một int chứ không phải char. (char là kiểu dữ liệu số nguyên nhỏ nhất)
PS:
Điều này không đúng với C++. Xem kết quả bên dưới.
$ gcc -o size size.c && ./size
sizeof(char) 1
sizeof(int) 4
sizeof('a') 4
$ g++ -o size size.c && ./size
sizeof(char) 1
sizeof(int) 4
sizeof('a') 1
Tìm hiểu và tài liệu học tập về ngôn ngữ lập trình C tại đây
1.Bài giảng học lập trình C pdf
2.Bài tập lệnh mảng 1 chiều ngôn ngữ lập trình C#
3.97 bài tập C++ cơ bản nâng cao hay nhất Phần 1