Tấn công con trỏ trong C (Phần 1)

Feb 02 2023
Con trỏ 101 Tôi nhớ đã học lập trình C với các bạn cùng trang lứa và cảm giác vui sướng mà chúng tôi cảm thấy cho đến khi giáo viên của chúng tôi đề cập đến con trỏ. Đó là một thử thách, nhưng thật thú vị đối với tôi khi hiểu được một khái niệm quan trọng như vậy.

Con trỏ 101

Hình ảnh của tôi

Tôi nhớ việc học lập trình C với các bạn cùng trang lứa và cảm giác vui sướng mà chúng tôi cảm nhận được cho đến khi giáo viên của chúng tôi đề cập đến các gợi ý. Đó là một thử thách, nhưng thật thú vị đối với tôi khi hiểu được một khái niệm quan trọng như vậy.

Con trỏ được cho là một trong những khái niệm phức tạp nhất trong ngôn ngữ C, nhưng chúng rất cần thiết nếu bạn muốn trở thành một lập trình viên C thành thạo. Ngay cả khi chúng ta không nhận ra điều đó, con trỏ vẫn ở xung quanh chúng ta trong hầu hết mọi mã chúng ta viết, rõ ràng hoặc ngầm định.

Trong bài viết này, chúng ta sẽ khám phá các tình huống sử dụng khác nhau cho con trỏ — tập trung vào những tình huống phổ biến nhất. Vì vậy, hãy hít một hơi thật sâu và bắt đầu nào!

Trước khi đọc bài viết này, hãy chắc chắn rằng bạn đã quen thuộc với những khái niệm cơ bản nhất của ngôn ngữ C: Biến và kiểu, điều kiện, vòng lặp và hàm. Trong phần đầu tiên của loạt bài này, đây là các khái niệm mà chúng ta sẽ tìm hiểu về con trỏ và biểu diễn bộ nhớ, khai báo và câu lệnh về con trỏ; địa chỉ và số học của con trỏ, ép kiểu và toán tử dereference.

Bộ nhớ máy tính

Quay trở lại khi, Con trỏ và Lập trình viên là một và giống nhau. Họ đã cùng nhau tạo ra đủ thứ. Nếu không có sự hợp tác của họ, sẽ không có gì được thực hiện. Vậy bạn nói sao… tại sao chúng ta không bẻ khóa?! Xin lỗi đừng chán

Bây giờ chúng ta hãy nghiêm túc!

Để hiểu con trỏ và các thao tác có thể thực hiện với chúng, chúng ta cần tìm hiểu một chút về bộ nhớ máy tính và cách các biến được định vị và biểu diễn trong đó. Máy tính có thể có nhiều gigabyte bộ nhớ. Mỗi byte bộ nhớ có một địa chỉ duy nhất. Chúng ta có thể coi bộ nhớ chính hoặc RAM là một mảng byte lớn và địa chỉ là chỉ mục của từng phần tử mảng hoặc byte trong mảng. Hầu hết các biến được lưu trữ trong mảng (nghĩa là trong bộ nhớ chính) lớn hơn một byte, vì vậy địa chỉ của mỗi biến là chỉ số của byte đầu tiên của biến đó.

Nguồn: Kích thước của các loại dữ liệu

Bạn có thể thấy ở trên các kiểu dữ liệu nguyên thủy phổ biến và kích thước của chúng có thể nhận được bằng sizeoftoán tử trong ngôn ngữ C. Bán tại:sizeof(int)

Bộ nhớ chính (RAM), có thể được hình dung dưới dạng một mảng byte liền kề. Một địa chỉ tương đương với một chỉ mục trong mảng bộ nhớ. Hầu hết các kiểu dữ liệu kéo dài nhiều byte bộ nhớ (ví dụ: trong ảnh đầu tiên, float kéo dài 4 byte và char kéo dài 1 byte). Địa chỉ của một biến nhiều byte là địa chỉ hoặc chỉ số mảng của byte đầu tiên của nó.

Bạn có thể coi một biến là ngôi nhà và địa chỉ là khóa để truy cập nội dung của ngôi nhà. Nội dung ngôi nhà (có thể là người cư ngụ hoặc bất cứ thứ gì bên trong) có thể thay đổi. Và bất kỳ ai có chìa khóa của một ngôi nhà đều có thể truy cập và thay đổi nội dung của nó.

Hình ảnh của tôi

Biến đầu tiên có kiểu int, vì vậy nó kéo dài 4 byte và địa chỉ của nó là chỉ mục của byte đầu tiên (0). Cái thứ hai là loại char (kéo dài 1 byte) và sau đó có địa chỉ 4 vì cái thứ nhất kéo dài bốn byte đầu tiên đếm từ 0 đến n-1 (0, 1, 2, 3).

Lưu ý: Địa chỉ của biến trong bộ nhớ chính thường được viết ở dạng thập lục phân (ví dụ: 0xFA44dE). Chúng tôi sử dụng số nguyên ở đây (0, 1,…) để đơn giản hóa ký hiệu. Khi bạn khai báo một biến x và một biến y , hai biến cùng loại hoặc khác loại, x có thể được lưu trữ tại địa chỉ 8 và y tại địa chỉ 465792. Người ta không nói rằng các biến của bạn nên được lưu trữ trong các địa chỉ liên tiếp . Chương trình của bạn kiểm tra địa chỉ có sẵn và miễn phí trước khi lưu các biến của bạn vào bộ nhớ. Vì vậy, nếu một địa chỉ không có sẵn hoặc không thể lưu trữ dữ liệu, thì chương trình hoặc hệ điều hành của bạn (bất kỳ thứ gì) có trách nhiệm lưu trữ biến của bạn tại một địa chỉ phù hợp.

Trong thực tế, khi bạn khai báo một biến và đặt giá trị cho nó, biến này được lưu trữ tại một địa chỉ với giá trị đã cho. Hãy xem ví dụ này:

Nếu vì lý do nào đó, bạn không thể mở liên kết ở trên, đây là mã trong hộp cát:

/*
* Auth: Arsene Awounou
* File: main.c
*/
#include <stdio.h>

/**
* main - entry point
*
* Return: always 0
*/
int main(void) {
  /* declare variables and initialize them*/
  int a = 4;
  int b = 17;
  char c = 'c';

  /* print the above variables */
  printf("%d %d %c\n", a, b, c);

  return 0;
}

Biểu diễn bộ nhớ của các biến (bởi tôi)

con trỏ

Con trỏ trong C là gì?

Trong ngôn ngữ lập trình C, con trỏ là một biến lưu trữ địa chỉ của một biến khác. Really ? Vâng, thực sự. Biết điều này có nghĩa là gì? Điều này có nghĩa là một con trỏ có các thuộc tính giống như một biến bình thường nhưng có đặc điểm lưu trữ địa chỉ của các biến khác. Do đó, một con trỏ được đặc trưng bởi tên của nó, kiểu của nó (tên của biến mà nó chứa địa chỉ) và địa chỉ của chính nó. Vâng, chúng tôi đã nói rằng một con trỏ cũng là một biến, vì vậy nó kế thừa tất cả các thuộc tính của một biến.

Làm cách nào để lấy địa chỉ của một biến?

Chúng tôi vừa nói rằng con trỏ có nhiệm vụ lưu trữ địa chỉ của biến. Do đó chúng ta cần biết cách lấy những địa chỉ đó, phải không? Để làm như vậy, chúng tôi sử dụng dấu & . Hãy xem đoạn mã sau:

Nếu vì lý do nào đó, bạn không thể mở liên kết ở trên, đây là mã trong hộp cát:

/*
* Auth: Arsene Awounou
* File: main.c
*/
#include <stdio.h>

/**
* main - entry point
*
* Return: always 0
*/
int main(void) {
  /* declare variables and initialize them*/
  int a = 4;
  int b = 17;
  char c = 'c';

  /* print the above variables */
  printf("Address of a is : %p\n", &a);
  printf("Address of b is : %p\n", &b);
  printf("Address of c is : %p\n", &c);

  return 0;
}

Đầu ra của mã trước đó

Lưu ý : Nếu bạn chạy cùng một mã trên máy của mình, bạn có thể nhận được một đầu ra khác. Hãy nhớ rằng, tôi đã nói với bạn rằng địa chỉ của các biến được hệ điều hành của bạn chọn tùy theo tính khả dụng của chúng. Vì vậy, trên máy của tôi, các địa chỉ/vị trí 0x7ffcc4b574c8 , 0x7ffcc4b574c4 và 0x7ffcc4b574c3 có sẵn để lưu trữ các biến của tôi.

Vì vậy, để lấy địa chỉ của một biến đã cho var , chúng tôi sử dụng &var. Này anh bạn, vâng, bạn đang đọc! Điều này không nhắc nhở bạn về bất cứ điều gì quen thuộc? Tôi khá chắc chắn rằng bạn đã từng viết một cái gì đó như thế này trước đây: scanf("%d", &var);Phải không?

Chúng tôi gửi địa chỉ của biến đến hàm scanf để nó có thể trực tiếp sửa đổi giá trị của nó. Giữ nó trong tâm trí:

Một biến có thể được so sánh với một ngôi nhà và một địa chỉ giống như chìa khóa cung cấp quyền truy cập vào nội dung bên trong. Nội dung này có thể thay đổi (chẳng hạn như khi chúng tôi thay đổi vị trí của đồ nội thất hoặc sơn lại) và bất kỳ ai giữ chìa khóa đều có quyền truy cập cũng như kiểm soát nội dung đó.

Làm cách nào để khai báo Biến con trỏ C?

Cách chung để khai báo một con trỏ là:

datatype* pointer_name;

datatype* pointer_name;
/* or */
datatype * pointer_name;

int* test, test_1; // example 1
int * test, test_1; // example 2
int *test, test_1; // example 3

int *test, *test_1, *test_2; // and so on

Khởi tạo con trỏ là quá trình gán địa chỉ của một biến cho một con trỏ:

int a = 17;
int *pointer_to_a = &a; /* declaration + initialization */

int a = 17;
int *pointer_to_a; // declaration
pointer_to_a = &a; // initialization

char c = 'A';
int *pointer_to_c = &c; // mismatching types 

Toán tử hủy tham chiếu *

Toán tử quy chiếu , còn được gọi là toán tử gián tiếp , ký hiệu là * trong ngôn ngữ C, là một toán tử đơn nguyên (tức là toán tử có toán hạng đơn) bao gồm biến con trỏ. Nó hoạt động trên một biến con trỏ và trả về và l-valuetương đương với giá trị tại địa chỉ con trỏ. Điều này được gọi là dereferencing con trỏ . Xét ví dụ sau để hiểu rõ hơn:

Nếu vì lý do nào đó, bạn không thể mở liên kết ở trên, đây là mã trong hộp cát:

/*
* Auth: Arsene Awounou
* File: main.c
*/
#include <stdio.h>

/**
* main - entry point
*
* Return: always 0
*/
int main(void) {
  int var = 17;
  int *ptr = &var; // assign ptr the address of var

  /* we can print the value of var in three different ways */
  printf("Value of var is : %d\n", var);
  printf("Value of var is : %d\n", *ptr);
  printf("Value of var is : %d\n", *(&var));

  /* we can print the address of var in two different ways */
  printf("Address of var is : %p\n", &var);
  printf("Address of var is : %p\n", ptr);

  /* we can print the address of ptr in this way */
  printf("Address of ptr is : %p\n", &ptr);

  return 0;
}

Đầu ra của mã trước đó

Giải trình :

  • ptrlà một con trỏ trỏ đến biến var (ptr lưu địa chỉ của biến đó : int *ptr = &var;) ;
  • Các biểu thức *ptrvà nghĩa giống nhau: chúng được sử dụng để lấy giá trị được lưu vartrữ *(&var)trong biến var.
  • Khi khai báo một biến con trỏ, * chỉ ra rằng biến đó là một con trỏ;
  • Chúng ta có thể lấy địa chỉ của một biến bằng cách sử dụng dấu & (ví dụ: &var) ;
  • Khai báo int *ptrcó nghĩa là ptr sẽ lưu trữ địa chỉ của một biến có kiểu số nguyên (lưu trữ một giá trị số nguyên);
  • Để truy cập giá trị tại một địa chỉ nhất định, * được sử dụng. Và * có thể được đọc là 'giá trị tại' (*ptr có nghĩa là giá trị tại ptr).

Để đảm bảo một môi trường an toàn, điều cần thiết là gán giá trị NULL cho các biến con trỏ nếu bạn không có địa chỉ chính xác. Điều này nên được thực hiện khi biến được khai báo để tạo ra cái được gọi là con trỏ null.

Con trỏ NULL là một hằng số có giá trị bằng 0 được xác định trong một số thư viện tiêu chuẩn.

#include <stdio.h>

int main () {
   int  *pointer = NULL;
   printf("Value of pointer: %x\n", pointer); // 0
 
   return 0;
}

Việc xác minh sự hiện diện của con trỏ null có thể được thực hiện bằng câu lệnh 'if' theo cách sau:

if(pointer)     /* instructions run if pointer is not null */
if(!pointer)    /* instructions run if pointer is null */

Cảm ơn bạn đã đọc.

Bạn có thể tự do liên hệ với tôi trên Twitter , LinkedIn , WhatsApp hoặc Telegram .