Little endian vs. Big endian


Link gốc: https://manhhomienbienthuy.github.io/2018/09/20/little-endian-vs-big-endian.html (đã được sự cho phép của tác giả)

Máy tính có hai phương thức lưu trữ dữ liệu nhị phân (binary) khác nhau, đó là nhỏ trước (little endian) và lớn trước (big endian). Thông thường, chúng ta không quan tâm đến cách thức lưu trữ này, vì tất cả được tự động hoá.

Tuy nhiên, có những trường hợp đặc biệt khi chúng ta phải xử lý các tập tin có cấu trúc hoặc các tập tin nhị phân, đặc biệt là khi chúng được tạo bởi các ngôn ngữ khác. Trong những trường hợp này, hiểu về little endian và big endian là rất quan trọng. Nếu không, chúng ta có thể đọc và xử lý các dữ liệu theo thứ tự sai.

Dữ liệu là biểu thị của thông tin dưới dạng lưu trữ. Thông tin là trừu tượng, không có hình dạng cụ thể, đó là hiểu biết về các đối tượng và sự kiện xung quanh chúng ta. Để lưu trữ và truyền tải thông tin cho mọi người, chúng ta cần sử dụng dữ liệu. Dữ liệu có thể là chữ viết, hình ảnh được ghi trên giấy, tất cả là những gì con người có thể hiểu được.

Tuy nhiên, dữ liệu cần phải được mã hóa để lưu trữ trên máy tính. Như chúng ta đã biết, máy tính chỉ làm việc với dữ liệu được mã hóa dưới dạng nhị phân, do đó mọi dữ liệu cần được mã hóa thành nhị phân trước khi có thể xử lý trên máy tính.

Thực tế, điều này chỉ đúng với máy tính số (máy tính điện tử số). Nghe nói hiện nay, máy tính lượng tử và máy tính sinh học cũng đang được phát triển, hy vọng rằng trong vài năm tới, chúng ta sẽ cập nhật kiến thức về dữ liệu.

Thực tế, máy tính không hiểu các ký tự “0” và “1” trong hệ nhị phân. Máy tính hoạt động dựa trên tín hiệu điện tử. Mô tả công việc này rất khó, nhưng chúng ta có thể hiểu “dễ dàng” rằng khi gặp bit “1”, sẽ có dòng điện và khi gặp bit “0”, sẽ không có dòng điện. Như vậy, các bit “0” và “1” được chuyển thành các tín hiệu điện tử tương ứng, và máy tính coi đó là dữ liệu nhị phân.

Mặc dù tất cả sử dụng cùng một hệ thống tín hiệu nhị phân, các máy tính khác nhau không thực sự “nói chung” một ngôn ngữ. Tương tự như con người, khi nhìn các ký tự “a”, “b”, “c”, có người hiểu và có người không hiểu. Máy tính khi nhìn vào các tín hiệu tương ứng với các ký tự “0” hoặc “1”, mỗi máy tính có thể hiểu theo một cách khác nhau.

Tuy nhiên, may mắn thay, các máy tính vẫn hoạt động theo các tiêu chuẩn chung, vì vậy chúng vẫn có thể giao tiếp với nhau. Tuy nhiên, cần lưu ý rằng, không phải lúc nào cũng các máy tính đều có thể hiểu lẫn nhau.

Trong máy tính, dữ liệu nhị phân không được xử lý theo từng bit riêng lẻ, mà được xử lý thành các khối 8 bit một, và đơn vị xử lý nhỏ nhất này được gọi là byte.

Ví dụ, số nguyên 123456789 khi biểu diễn dưới dạng nhị phân sẽ là (ở đây tôi cho rằng kiểu dữ liệu int có kích thước là 4 byte, tuy nhiên, nhiều hệ thống 64 bit đã nâng kích thước này lên 8 byte)

00000111 01011011 11001101 00010101

Để ngắn gọn, chúng ta có thể viết nó dưới dạng hexa như sau:

07 5b cd 15

Bạn đã từng tự hỏi, khi ghi dữ liệu này trên đĩa cứng chẳng hạn, nó được ghi như thế nào? Bạn có nghĩ rằng nó sẽ được ghi theo thứ tự mà chúng ta đang đọc và viết ở trên, nhưng bạn đã sai.

Đây chỉ là cách viết để chúng ta dễ hiểu, máy tính không “đọc” các ký tự giống như chúng ta, vì vậy nó cũng không lưu trữ giống cách chúng ta viết các ký tự này. Cách mã hoá dữ liệu như thế nào chính là lúc mà little endian và big endian được sử dụng.

Có Thể Bạn Quan Tâm :   Căn cứ lập kế hoạch đầu tư công trung hạn và hằng năm

Little endian và big endian là hai phương thức khác nhau để lưu trữ dữ liệu. Sự khác biệt giữa little endian và big endian nằm ở việc sắp xếp các byte dữ liệu.

Trong cơ chế lưu trữ little endian (xuất phát từ “little-end” nghĩa là kết thúc nhỏ hơn), byte cuối cùng trong biểu diễn nhị phân sẽ được ghi trước. Ví dụ, khi sử dụng little endian, số 123456789 sẽ được ghi theo thứ tự

15 cd 5b 07

Ngược lại, big endian (xuất phát từ “big-end”) sẽ ghi dữ liệu theo thứ tự bình thường mà chúng ta vẫn sử dụng. Ví dụ, 123456789 vẫn được lưu trữ theo đúng thứ tự là

07 5b cd 15

Các thuật ngữ big-end hoặc little-end đã xuất phát từ cuốn tiểu thuyết Gulliver du ký (Gulliver’s Travels), trong đó nhân vật Lilliputans tranh luận về việc nên đập trứng bằng đầu to hay nhỏ.

Ngành công nghệ thông tin đã áp dụng thuật ngữ này, tương đối giống với ý nghĩa ban đầu. Lưu ý rằng, little endian và big endian chỉ khác nhau ở cách sắp xếp các byte dữ liệu, còn thứ tự từng bit trong byte thì giống nhau. May mắn là các máy tính vẫn có điểm chung này.

Thêm một lưu ý nữa là, little endian và big endian chỉ khác nhau khi lưu trữ các dữ liệu có nhiều byte. Các dữ liệu chỉ có 1 byte (như ký tự ASCII) thì không bị ảnh hưởng (thực ra có thể sử dụng phương thức nào cũng cho kết quả giống nhau).

Việc sắp xếp các byte dữ liệu theo kiểu little endian hay big endian không chỉ xảy ra khi lưu trữ dữ liệu ra bộ nhớ ngoài. Mọi hoạt động của máy tính đều sử dụng dữ liệu nhị phân, nên little endian/big endian tồn tại trong mọi hoạt động của máy tính.

Ngoài ra, việc sử dụng little endian/big endian phụ thuộc vào phần mềm (do lập trình viên tự ý sử dụng một trong hai loại hoặc ngôn ngữ lập trình quy định trước) và cũng phụ thuộc vào bộ vi xử lý của máy tính đó.

Các bộ vi xử lý Intel sử dụng little endian, trong khi các bộ vi xử lý ARM trước đây cũng sử dụng little endian. Tuy nhiên, hiện nay bộ vi xử lý ARM đã nâng cấp để hỗ trợ cả little endian và big endian.

Các bộ vi xử lý PowerPC và SPARC ban đầu sử dụng big endian, nhưng hiện nay chúng cũng đã nâng cấp để hỗ trợ cả little endian và big endian.

Little endian và big endian không có phương pháp nào tốt hơn phương pháp nào. Cả hai phương pháp không ảnh hưởng đến tốc độ xử lý của CPU. Do đó, cả hai phương pháp vẫn tồn tại song song và không thể trả lời câu hỏi: Phương pháp nào tốt hơn?

Mỗi phương pháp có những ưu điểm riêng. Với little endian, vì byte nhỏ nhất luôn nằm bên trái, nó cho phép chúng ta đọc dữ liệu với độ dài bất kỳ. Nó rất phù hợp khi chúng ta cần ép kiểu, ví dụ từ int thành long int.

Giả sử int có kích thước là 4 byte, long int có kích thước là 8 byte, khi sử dụng little endian, khi ép kiểu, địa chỉ bộ nhớ không cần thay đổi, chúng ta chỉ cần ghi tiếp các byte có kích thước lớn hơn.

Tuy nhiên, trong cùng trường hợp đó, nếu sử dụng big endian, chúng ta sẽ phải dịch địa chỉ bộ nhớ hiện tại thêm 4 byte nữa để có không gian để lưu trữ.

Tuy vậy, big endian cũng có những ưu điểm riêng. Với việc đọc dữ liệu byte lớn nhất trước, nó dễ dàng kiểm tra một số là âm hay dương, do byte chứa dấu được đọc đầu tiên.

Một chương trình C đơn giản cho thấy cách xếp byte trong bộ nhớ:

#include <stdio.h>
/* chương trình hiển thị dữ liệu trong bộ nhớ, từ vị trí start đến start+n */
void show_mem_rep(char *start, int n)
{
    int i;
    for (i = 0; i < n; i++)
        printf(" %.2x", start[i]);
    printf("n");
}
/* Hàm chính để gọi hàm trên cho giá trị 0x01234567 */
int main()
{
    int i = 0x01234567;
    show_mem_rep((char *)&i, sizeof(i));
    return 0;
}

Khi chạy chương trình trên, nếu máy tính của bạn là little endian, kết quả sẽ là:

Có Thể Bạn Quan Tâm :  

67 45 23 01

Còn nếu máy tính của bạn là big endian, nó sẽ hiển thị theo thứ tự thông thường:

01 23 45 67

Có cách nào để xác định máy tính của chúng ta là little endian hay big endian không? Có rất nhiều cách khác nhau, dưới đây là một trong số đó:

#include <stdio.h>
int main()
{
    unsigned int i = 1;
    char *c = (char *)&i;
    if (*c)
        printf("Little endian");
    else
        printf("Big endian");
    return 0;
}

Trong đoạn code trên, c là một con trỏ, nó trỏ đến vùng nhớ của biến i là một số nguyên. Vì số nguyên là kiểu dữ liệu nhiều byte, trong khi dữ liệu của char chỉ là một byte, nên *c sẽ trả về giá trị là byte đầu tiên của số nguyên i.

Nếu máy tính của chúng ta là little endian, byte đầu tiên này sẽ là 1; ngược lại, nó sẽ là 0.

Về cơ bản, little endian hoặc big endian không có ảnh hưởng đáng kể đến việc lập trình. Phần lớn lập trình viên không cần quan tâm nhiều về điều này, vì các trình biên dịch/thông dịch đảm nhận tất cả công việc.

Tuy nhiên, có những trường hợp đặc biệt mà chúng ta cần quan tâm, đặc biệt khi chuyển đổi dữ liệu giữa các máy tính khác nhau. Ví dụ, khi chúng ta cần xử lý một tệp có cấu trúc như sau: 4 byte đầu tiên là một số nguyên n, sau đó là n số nguyên, mỗi số chiếm 4 byte bộ nhớ, v.v…

Trong trường hợp này, khi nhận tệp được tạo ra từ một máy tính khác, cách nó được ghi theo little endian hoặc big endian là rõ ràng ảnh hưởng rất lớn, và nếu chúng ta sử dụng một phương pháp sai, chúng ta sẽ nhận được dữ liệu sai.

Một trường hợp khác có thể gặp vấn đề là khi chúng ta convert kiểu dữ liệu:

#include <stdio.h>
int main()
{
    unsigned char arr[2] = {0x01, 0x00};
    unsigned short int x = *(unsigned short int *)arr;
    printf("%d", x);
    return 0;
}

Trong đoạn code trên, chúng ta đã convert một mảng hai phần tử char thành một số nguyên 2 byte (short int). Trong ví dụ này, little endian sẽ cho kết quả là 1, trong khi big endian sẽ là 256. Để tránh những lỗi không mong muốn, chúng ta cần tránh những đoạn code như trên.

NUXI là một vấn đề nổi tiếng liên quan đến little endian và big endian: UNIX được lưu trữ trong một hệ thống big-endian sẽ được hiểu là NUXI trong một hệ thống little-endian.

Giả sử chúng ta cần lưu trữ 4 byte (U, N, I, X) bằng hai số nguyên 2 byte: UN và IX.

#include <stdio.h>
int main()
{
    short int *s; // con trỏ trỏ vào shorts
    s = (short int *)malloc(sizeof(short int)); // trỏ đến vị trí số 0
    *s = "UN"; // lưu trữ short đầu tiên: U * 256 + N (đoạn code giả)
    s += 2; // trỏ đến vị trí tiếp theo
    *s = "IX"; // lưu trữ short thứ hai: I * 256 + X
    return 0;
}

Đoạn code trên độc lập với hệ thống, dù là little endian hay big endian. Nếu chúng ta lưu trữ giá trị “UN” và “IX” và sau đó đọc chúng, nó sẽ vẫn là “UNIX”. Nếu mọi việc chỉ xảy ra trên một máy tính, dù big endian hay little endian, nó sẽ luôn như vậy, bởi vì mọi thứ được tự động thực hiện giúp chúng ta.

Với bất kỳ dữ liệu nào, chúng ta luôn nhận được dữ liệu đúng nếu đọc và ghi trong cùng một hệ thống. Tuy nhiên, hãy xem xét kỹ hơn về cách sắp xếp các byte trong bộ nhớ.

Một hệ thống big endian sẽ lưu trữ như sau:

U N I X

Còn một hệ thống little endian sẽ như sau:

N U X I

Mặc dù trông có vẻ ngược nhau, nhưng hệ thống little endian sẽ tự động thực hiện việc đọc, vì vậy lưu trữ như vậy, nhưng khi lấy ra, chúng ta vẫn có dữ liệu ban đầu. Tuy nhiên, khi ghi dữ liệu này ra tệp tin, và chuyển sang máy tính khác. Mỗi máy tính sẽ xử lý theo cách của riêng nó. Và lúc đó, UNIX trên máy tính big endian sẽ được hiểu là NUXI trên máy tính little endian (và ngược lại).

Có Thể Bạn Quan Tâm :   Thơ là gì? Đặc trưng và Phân loại thơ

Đây là vấn đề nguy hiểm nhất khi trao đổi dữ liệu giữa các máy tính với nhau, đặc biệt là trong thời đại của Internet ngày nay.

Ngày nay, mọi máy tính được kết nối để trao đổi dữ liệu. Little endian và big endian cần thiết để trao đổi dữ liệu với nhau, nhưng làm thế nào để có thể hiểu nhau khi chúng không nói cùng một ngôn ngữ?

Có hai giải pháp chính cho vấn đề này:

Sử dụng cùng định dạng

Một giải pháp đơn giản là sử dụng cùng một định dạng khi truyền dữ liệu.

Ví dụ, tất cả các tệp tin dạng PNG đều yêu cầu sử dụng big endian. Tương tự, các tệp tin có cấu trúc khác cũng theo quy tắc tương tự. Đó là lý do tại sao chúng ta thỉnh thoảng cần sử dụng các phần mềm chuyên dụng để đọc và ghi các tệp tin này.

Thế nhưng, trong kết nối Internet, việc truyền dữ liệu không đơn giản như vậy. Chúng ta không thể chỉ sử dụng một định dạng tệp tin nào đó rồi truyền từng byte một sang máy tính khác. Để tăng tốc độ, chúng ta cần truyền nhiều byte cùng một lúc.

Trong trường hợp đó, chúng ta cần có một chuẩn chung. Hiện nay, chuẩn chung cho việc truyền dữ liệu trên mạng, gọi là network byte order, chính là big endian. Tuy nhiên, dù đã có chuẩn chung, vẫn có những giao thức đặc biệt hơn, sử dụng little endian.

Để chuyển đổi dữ liệu thành dữ liệu trong network byte order, chương trình cần gọi hàm hton* (host-to-network) (trong ngôn ngữ C). Trong hệ thống big endian, hàm này không cần làm gì cả, trong khi little endian có thể thực hiện chuyển đổi các byte một chút.

Mặc dù hệ thống big endian không cần chuyển đổi dữ liệu, việc gọi hàm này vẫn cần thiết. Chương trình của chúng ta có thể được viết bằng một ngôn ngữ (như C) nhưng có thể biên dịch và thực thi trên nhiều hệ thống khác nhau, và gọi hàm này sẽ giúp chúng ta làm điều đó.

Tương tự, ở chiều ngược lại, chúng ta cần gọi hàm ntoh* để chuyển đổi dữ liệu nhận được từ mạng thành dữ liệu mà máy tính có thể hiểu. Ngoài ra, chúng ta cũng cần biết rõ kiểu dữ liệu mà chúng ta cần chuyển đổi, danh sách các hàm chuyển đổi như sau:

  • htons – “Host to Network Short” (chuyển đổi short từ máy tính thành định dạng network)
  • htonl – “Host to Network Long” (chuyển đổi long từ máy tính thành định dạng network)
  • ntohs – “Network to Host Short” (chuyển đổi short từ định dạng network thành máy tính)
  • ntohl – “Network to Host Long” (chuyển đổi long từ định dạng network thành máy tính)

Những hàm này rất quan trọng khi thực hiện chia sẻ dữ liệu ở lớp thấp, chẳng hạn như khi kiểm tra checksum của các gói tin. Nếu không hiểu rõ về little endian và big endian, bạn sẽ gặp nhiều khó khăn khi làm việc với mạng.

Sử dụng BOM (Byte Order Mark)

Một giải pháp khác để giải quyết sự khác biệt endian là sử dụng BOM (Byte Order Mark). Đây là một ký tự đặc biệt, có giá trị là 0xFEFF, được ghi ở vị trí đầu tiên của tệp tin.

Nếu bạn đọc ký tự này là 0xFFFE (bị ngược), thì nghĩa là tệp tin này được ghi với endian khác với hệ thống của bạn, khi đó bạn cần phải thay đổi cách đọc dữ liệu một chút.

Tuy nhiên, việc sử dụng BOM có một số vấn đề nhỏ. Thứ nhất, BOM sẽ làm tăng kích thước của dữ liệu ghi vào tệp tin. Ngay cả khi chúng ta chỉ gửi 2 byte dữ liệu, chúng ta vẫn cần thêm 2 byte BOM.

Thứ hai, BOM không hoàn toàn tin cậy, bởi nó phụ thuộc vào lập trình viên. Có người kiến thức cao sẽ đọc và xử lý khi gặp BOM, trong khi có người hoàn toàn quên nó và coi nó như dữ liệu bình thường. Unicode sử dụng BOM khi lưu trữ dữ liệu nhiều byte (nhiều ký tự Unicode được mã hoá thành 2, 3 hoặc 4 byte).

Back to top button