9.9. structs trong Assembly
Một struct
là một cách khác để tạo ra một tập hợp các kiểu dữ liệu trong C.
Không giống như mảng, struct
cho phép nhóm nhiều kiểu dữ liệu khác nhau lại với nhau.
C lưu trữ một struct
giống như một mảng một chiều, trong đó các phần tử dữ liệu (các field) được lưu trữ liên tiếp nhau trong bộ nhớ.
Hãy cùng xem lại struct studentT
từ Chương 1:
struct studentT {
char name[64];
int age;
int grad_yr;
float gpa;
};
struct studentT student;
Hình 1 cho thấy cách student
được bố trí trong bộ nhớ. Mỗi a~i~ biểu thị một offset trong bộ nhớ.
Hình 1. Sơ đồ bố trí bộ nhớ của một struct studentT
.
Mỗi field được lưu liên tiếp nhau trong bộ nhớ theo đúng thứ tự khai báo.
Trong Hình 1, field age
được cấp phát ngay sau field name
(tại byte offset a~64~), tiếp theo là grad_yr
(byte offset a~68~) và gpa
(byte offset a~72~).
Cách tổ chức này cho phép truy cập các field một cách hiệu quả về mặt bộ nhớ.
Để hiểu cách compiler sinh code assembly làm việc với một struct
, hãy xem xét hàm initStudent
:
void initStudent(struct studentT *s, char *nm, int ag, int gr, float g) {
strncpy(s->name, nm, 64);
s->grad_yr = gr;
s->age = ag;
s->gpa = g;
}
Hàm initStudent
nhận địa chỉ cơ sở của một struct studentT
làm tham số đầu tiên,
và các giá trị mong muốn cho từng field làm các tham số còn lại.
Danh sách dưới đây là code assembly tương ứng của hàm:
Dump of assembler code for function initStudent:
0x7f4 <+0>: stp x29, x30, [sp, #-48]! // sp -= 48; lưu x29, x30 tại sp, sp+4
0x7f8 <+4>: mov x29, sp // x29 = sp (frame pointer = stack pointer)
0x7fc <+8>: str x0, [x29, #40] // lưu s tại x29 + 40
0x800 <+12>: str x1, [x29, #32] // lưu nm tại x29 + 32
0x804 <+16>: str w2, [x29, #28] // lưu ag tại x29 + 28
0x808 <+20>: str w3, [x29, #24] // lưu gr tại x29 + 24
0x80c <+24>: str s0, [x29, #20] // lưu g tại x29 + 20
0x810 <+28>: ldr x0, [x29, #40] // x0 = s
0x814 <+32>: mov x2, #0x40 // x2 = 0x40 (64)
0x814 <+36>: ldr x1, [x29, #32] // x1 = nm
0x818 <+40>: bl 0x6e0 <strncpy@plt> // gọi strncpy(s, nm, 64) (s->name)
0x81c <+44>: ldr x0, [x29, #40] // x0 = s
0x820 <+48>: ldr w1, [x29, #24] // w1 = gr
0x824 <+52>: str w1, [x0, #68] // lưu gr tại (s + 68) (s->grad_yr)
0x828 <+56>: ldr x0, [x29, #40] // x0 = s
0x82c <+60>: ldr w1, [x29, #28] // w1 = ag
0x830 <+64>: str w1, [x0, #64] // lưu ag tại (s + 64) (s->age)
0x834 <+68>: ldr x0, [x29, #40] // x0 = s
0x838 <+72>: ldr s0, [x29, #20] // s0 = g
0x83c <+80>: str s0, [x0, #72] // lưu g tại (s + 72) (s->gpa)
0x844 <+84>: ldp x29, x30, [sp], #48 // khôi phục x29, x30; sp += 48
0x848 <+88>: ret // return (void)
Việc chú ý đến byte offset của từng field là chìa khóa để hiểu đoạn code này.
Dưới đây là một vài điểm cần lưu ý:
- Lời gọi
strncpy
nhận ba đối số: địa chỉ cơ sở của fieldname
trongs
, địa chỉ của mảngnm
, và một length specifier (chỉ định độ dài).
Hãy nhớ rằng vìname
là field đầu tiên trongstruct studentT
, nên địa chỉ củas
cũng chính là địa chỉ củas→name
.
0x7fc <+8>: str x0, [x29, #40] // lưu s tại x29 + 40
0x800 <+12>: str x1, [x29, #32] // lưu nm tại x29 + 32
0x804 <+16>: str w2, [x29, #28] // lưu ag tại x29 + 28
0x808 <+20>: str w3, [x29, #24] // lưu gr tại x29 + 24
0x80c <+24>: str s0, [x29, #20] // lưu g tại x29 + 20
0x810 <+28>: ldr x0, [x29, #40] // x0 = s
0x814 <+32>: mov x2, #0x40 // x2 = 0x40 (64)
0x814 <+36>: ldr x1, [x29, #32] // x1 = nm
0x818 <+40>: bl 0x6e0 <strncpy@plt> // gọi strncpy(s, nm, 64) (s->name)
-
Đoạn code trên có sử dụng một thanh ghi chưa được đề cập trước đó (
s0
). Thanh ghis0
là ví dụ về thanh ghi dành riêng cho giá trị floating point. -
Phần tiếp theo (các lệnh
<initStudent+44>
đến<initStudent+52>
) ghi giá trị của tham sốgr
vào vị trí cách đầus
68 byte.
Xem lại sơ đồ bố trí bộ nhớ của struct trong Hình 1 cho thấy địa chỉ này tương ứng vớis→grad_yr
.
0x81c <+44>: ldr x0, [x29, #40] // x0 = s
0x820 <+48>: ldr w1, [x29, #24] // w1 = gr
0x824 <+52>: str w1, [x0, #68] // lưu gr tại (s + 68) (s->grad_yr)
- Phần tiếp theo (các lệnh
<initStudent+56>
đến<initStudent+64>
) sao chép tham sốag
vào fields→age
, nằm tại offset 64 byte tính từ địa chỉ củas
.
0x828 <+56>: ldr x0, [x29, #40] // x0 = s
0x82c <+60>: ldr w1, [x29, #28] // w1 = ag
0x830 <+64>: str w1, [x0, #64] // lưu ag tại (s + 64) (s->age)
- Cuối cùng, giá trị tham số
g
được sao chép vào fields→gpa
(byte offset 72).
Lưu ý việc sử dụng thanh ghis0
vì dữ liệu tại vị tríx29 + 20
là số thực dấu phẩy động đơn chính xác (single-precision floating point):
0x834 <+68>: ldr x0, [x29, #40] // x0 = s
0x838 <+72>: ldr s0, [x29, #20] // s0 = g
0x83c <+80>: str s0, [x0, #72] // lưu g tại (s + 72) (s->gpa)
9.9.1. Data Alignment và structs
Xem xét khai báo studentT
đã được chỉnh sửa như sau:
struct studentTM {
char name[63]; // thay đổi thành 63 thay vì 64
int age;
int grad_yr;
float gpa;
};
struct studentTM student2;
Kích thước của field name
được thay đổi thành 63 byte thay vì 64 byte như ban đầu.
Hãy xem điều này ảnh hưởng thế nào đến cách struct
được bố trí trong bộ nhớ.
Có thể bạn sẽ hình dung nó như trong Hình 2:
Hình 2. Bố trí bộ nhớ sai cho struct studentTM
đã cập nhật. Lưu ý rằng field "name"
giảm từ 64 xuống 63 byte.
Trong hình minh họa này, field age
xuất hiện ngay sau field name
. Nhưng điều này là sai.
Hình 3 cho thấy bố trí thực tế trong bộ nhớ:
Hình 3. Bố trí bộ nhớ đúng cho struct studentTM
đã cập nhật.
Byte a~63~ được compiler thêm vào để đáp ứng yêu cầu memory alignment, nhưng nó không thuộc về bất kỳ field nào.
Chính sách alignment của A64 yêu cầu:
- Các kiểu dữ liệu 4 byte (ví dụ:
int
) phải nằm ở địa chỉ là bội số của 4. - Các kiểu dữ liệu 64-bit (
long
,double
, và con trỏ) phải nằm ở địa chỉ là bội số của 8.
Đối với một struct
, compiler sẽ thêm các byte trống gọi là padding giữa các field để đảm bảo mỗi field thỏa coden yêu cầu alignment.
Ví dụ, trong struct
ở đoạn code trên, compiler thêm 1 byte padding tại byte a~63~ để đảm bảo field age
bắt đầu ở địa chỉ là bội số của 4.
Các giá trị được align đúng trong bộ nhớ có thể được đọc hoặc ghi chỉ với một thao tác, giúp tăng hiệu suất.
Xem điều gì xảy ra khi struct
được định nghĩa như sau:
struct studentTM {
int age;
int grad_yr;
float gpa;
char name[63];
};
struct studentTM student3;
Việc chuyển mảng name
xuống cuối struct sẽ dời byte padding xuống cuối struct, đảm bảo age
, grad_yr
và gpa
đều được align theo 4 byte.