【C++ 小问答】5:对结构体成员字符数组的访问

 1struct Book {
 2	char name[10];
 3	char type[10];
 4	int price;
 5};
 6
 7struct Book *getBook()
 8{
 9    struct Book b = {
10        .name = "C Primer",
11        .type = "Programme",
12        .price = 100,
13    };
14    return &b;
15};
16
17int main()
18{
19    struct Book *b = getBook();
20    char *name = b->name;
21    char *type = b->type;
22    int price = b->price;
23
24    printf("name = %s\n", name);
25    printf("type = %s\n", type);
26    printf("price = %d\n", price);
27
28    free(b);
29}

代码的 main 函数中,会发生指针访问错误的语句有哪些?(多选题)

  1. struct Book *b = get_struct();
  2. char *name = b->name;
  3. char *type = b->type;
  4. int price = b->price;
  5. printf("name = %s\n", name);
  6. printf("type = %s\n", type);
  7. printf("price = %d\n", price);
  8. free(b);

4、5、6、8

解答:

“b 返回的是一个错误地址,偏移后的 b->price 的地址也是错误的,根据这个地址去访存的时候就会出错”

“那 2 和 3 为什么不是错的?”

“因为 b->nameb->type 计算完偏移地址后就直接赋值了,即使地址是错的,但是不会去访存”

这里主要是因为 nametypechar [10] 类型。用同样的字符串初始化,其保存内容的方式与 char * 不同。

字符数组

 1b, b->name ──►┌──────────┐
 2              │          │
 3              │          │
 4              │ char[10] │
 5              │          │
 6              │          │
 7   b->type ──►├──────────┤
 8              │          │
 9              │          │
10              │ char[10] │
11              │          │
12              │          │
13              ├──────────┤
14   b->price = │   int    │
15              └──────────┘

数组所声明的内存空间都会被纳入结构体中,结构体的体积也会因此变得“硕大无比”。整个数组空间都在栈上,初始化时,初始值会被加载到栈区的指定位置上。

1; struct Book b = { ... };
2        movabs  rax, 8243114992628408387
3        mov     QWORD PTR [rbp-48], rax
4        mov     WORD PTR [rbp-40], 0
5        movabs  rax, 7885065666585129552
6        mov     QWORD PTR [rbp-38], rax
7        mov     WORD PTR [rbp-30], 101
8        mov     DWORD PTR [rbp-28], 100

因此 b->nameb->type 的值只要对 b 加上相应的偏移就可以获取,不需要对 b 进行解引用("*")操作。

 1; // b is stored at [rbp-8]
 2; char *name = b->name;
 3        mov     rax, QWORD PTR [rbp-8]    ; b and b->name points to the same place
 4        mov     QWORD PTR [rbp-16], rax   ; so just store it unchanged
 5; char *type = b->type;
 6        mov     rax, QWORD PTR [rbp-8]    ; get b
 7        add     rax, 10                   ; shift b by 10 bytes (get b->type)
 8        mov     QWORD PTR [rbp-24], rax   ; store b+10 (as a local variable)
 9; int price = b->price;
10        mov     rax, QWORD PTR [rbp-8]
11        mov     eax, DWORD PTR [rax+20]
12        mov     DWORD PTR [rbp-28], eax

字符指针

1                              ┌──────────────┐
2                          ┌──►│  "C Primer"  │
3           ┌──────────┐   │   └──────────────┘
4 b->name = │  char *  ├───┘
5           ├──────────┤       ┌───────────────┐
6 b->type = │  char *  ├──────►│  "Programme"  │
7           ├──────────┤       └───────────────┘
8b->price = │   int    │
9           └──────────┘

结构体中只保存指针,字符串内容则保存在程序的数据段中。初始化的过程则是将指针指向字符串的首地址。

1.LC0:
2        .string "C Primer"
3.LC1:
4        .string "Programme"
5
6; struct Book b = { ... };
7        mov     QWORD PTR [rbp-32], OFFSET FLAT:.LC0
8        mov     QWORD PTR [rbp-24], OFFSET FLAT:.LC1
9        mov     DWORD PTR [rbp-16], 100

这种情况下,获取 b->nameb->type 就需要对 b 及其偏移后的指针进行解引用。

 1; char *name = b->name;
 2        mov     rax, QWORD PTR [rbp-8]    ; get value of b->name
 3        mov     rax, QWORD PTR [rax]      ; dereference b->name
 4        mov     QWORD PTR [rbp-16], rax   ; save what it gets as a local variable
 5; char *type = b->type;
 6        mov     rax, QWORD PTR [rbp-8]
 7        mov     rax, QWORD PTR [rax+8]    ; b+8 to get b->type and dereference
 8        mov     QWORD PTR [rbp-24], rax
 9; int price = b->price;
10        mov     rax, QWORD PTR [rbp-8]
11        mov     eax, DWORD PTR [rax+16]
12        mov     DWORD PTR [rbp-28], eax

因此,若 struct Book 中的 nametype 改为字符指针,则 2 和 3 也会变得和 4 一样要去访问内存,执行也会出错。

参考链接

以上汇编结果来自:Compiler Explorer


Linux 内核问题调试手段
Search Paths for Dynamic Linking & Loading