Machine data activity
输入gdb ./data-layout
,开启Data activity
之旅。
1 Integers and Local Variables
returnOne
的 C 语言版本如下:
int returnOne (void) {
int local = -1;
return abs(local);
}
输入disassemble returnOne
,得到如下结果:
(gdb) disassemble returnOne
Dump of assembler code for function returnOne:
0x0000000000400581 <+0>: sub $0x8,%rsp
0x0000000000400585 <+4>: mov $0xffffffff,%edi
0x000000000040058a <+9>: callq 0x400613 <abs>
0x000000000040058f <+14>: add $0x8,%rsp
0x0000000000400593 <+18>: retq
End of assembler dump.
可以看到首先我们为returnOne
预留了 8 个字节的栈空间。将0xffffffff
放入%edi
(应该是传入函数的第一个参数寄存器)。然后调用abs
,最后还原栈空间。
对abs
进行反汇编结果如下:
(gdb) disassemble abs
Dump of assembler code for function abs:
0x0000000000400613 <+0>: mov %edi,%edx
0x0000000000400615 <+2>: sar $0x1f,%edx
0x0000000000400618 <+5>: mov %edi,%eax
0x000000000040061a <+7>: xor %edx,%eax
0x000000000040061c <+9>: sub %edx,%eax
0x000000000040061e <+11>: retq
End of assembler dump.
abs
所做的是把%edi
放入%edx
,然后将其右移 31 位(也就是让符号位充满),将其和原值相异或,然后再减去全符号位。最后返回%eax
。此处local
变量应该是存储在寄存器%edi
中。这里的问题是如果local
不在栈上,那么我们就没有办法获取它的地址。如果需要local
的地址的话,我们可能需要将其压在栈上,然后使用leaq %rsp, %rdi
获取它的地址。
为了验证我们的结论,我们对returnOneTwo
进行反汇编:
(gdb) disassemble returnOneTwo
Dump of assembler code for function returnOneTwo:
0x0000000000400594 <+0>: sub $0x18,%rsp
0x0000000000400598 <+4>: movl $0xffffffff,0xc(%rsp)
0x00000000004005a0 <+12>: lea 0xc(%rsp),%rdi
0x00000000004005a5 <+17>: callq 0x40061f <absp>
0x00000000004005aa <+22>: add $0x18,%rsp
0x00000000004005ae <+26>: retq
End of assembler dump.
该函数所做的是预留 24 个字节的空间,然后把0xffffffff
放入M[%rsp + 12]
的位置上,并且将指向该位置的指针赋给%rdi
,随后调用absp
。由此推测,absp
的传入参数是一个指针(%rdi
中)。调用完成后恢复栈空间。
因此,absp
的函数原型应该是int absp(int* p)
。
2 Arrays
x
的几种不同格式的显示见此。这里的显示需要指明 3 个内容:
- 显示几个单位
- 每个单位几个字节:如 b=1 byte, h=2 bytes,w=4 bytes,g=8 bytes(如果不指定,默认 1 字节)
- 用几进制显示:
b
就是 2 进制,x
就是 16 进制,d
就是 10 进制
使用x/4b courses
只能查看 4 个字节,以 10 进制数显示,结果如下:
(gdb) x/4b courses
0x601110 <courses>: 19 82 1 0
使用x/4x courses
只能查看 4 个字节,以 16 进制数显示,结果如下:
(gdb) x/4x courses
0x601110 <courses>: 0x13 0x52 0x01 0x00
使用x/4wx courses
查看courses
处的 4 个int
类型值。w
是 4 个字节。结果如下:
(gdb) x/4wx courses
0x601110 <courses>: 0x00015213 0x00015513 0x00018213 0x00018600
可以看到courses
处存放了 4 个值:15213 15513 18213 18600,对应数组中的 4 个元素。
getNth
函数的 C 语言形式如下:
int getNth(int *arr , size_t index) {
return arr[index];
}
其中%rdi
是数组的起始位置(arr
),%rsi
是索引(index
)。因为一个int
类型是 4 个字节,因此计算地址的时候是*(rdi + 4 * rsi)
对getNth
函数进行反汇编可得:
(gdb) disassemble getNth
Dump of assembler code for function getNth:
0x00000000004005af <+0>: mov (%rdi,%rsi,4),%eax
0x00000000004005b2 <+3>: retq
End of assembler dump.
重新运行,输入一次c
,输入x/bx $rdi
可得:
(gdb) x/s $rdi
0x4007f8: "15213 CSAPP"
此处的字符串是15213 CSAPP
。(推测x/s $rdi
的意思以字符串的形式打印从$rdi
开始的内存)。输入x/12bx $rdi
,可以得到如下结果:
(gdb) x/12bx $rdi
0x4007f8: 0x31 0x35 0x32 0x31 0x33 0x20 0x43 0x53
0x400800: 0x41 0x50 0x50 0x00
字符串的结尾是0x00
,x/s
命令应该是通过0x00
判断字符串的结尾,进而计算字符串的长度的。
3 Structs
定义struct course
如下:
struct course {
int cs_ugrad;
int cs_grad;
int ece_ugrad ;
int ece_grad;
};
继续输入c
,handout 提示断点处的函数将struct course
结构的值作为传入参数,使用 x/4wx $rdi
查看%rdi
处的内存可以看到:
(gdb) x/4wx $rdi
0x601100 <course>: 0x00015213 0x00015513 0x00018213 0x00018600
我们可以发现该处的四个值就是:0x15213 0x15513 0x18213 0x18600
我们定义结构体:
struct increasing {
char a;
short b;
int c;
long d;
};
假设
a = 0x0a;
b = 0x0b0b;
c = 0x0c0c0c0c;
d = 0x0d0d0d0d0d0d0d0d;
继续输入c
,我们使用x/32bx $rdi
来查看结构体内容,内容如下:
(gdb) x/32bx $rdi
0x6010e0 <increasing>: 0x0a 0x00 0x0b 0x0b 0x0c 0x0c 0x0c 0x0c
0x6010e8 <increasing+8>: 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d
0x6010f0 <increasing+16>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x6010f8 <increasing+24>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
和我们料想的情况一致。
将结构体内容重新排列如下:
struct rearranged {
char a;
long b;
short c;
int d;
};
这种方法按理论来说应该比上面那种多用 8 个字节。结果验证如下:
(gdb) x/32bx rearranged
0x6010a0 <rearranged>: 0x0a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x6010a8 <rearranged+8>: 0x0b 0x0b 0x0b 0x0b 0x0b 0x0b 0x0b 0x0b
0x6010b0 <rearranged+16>: 0x0c 0x0c 0x00 0x00 0x0d 0x0d 0x0d 0x0d
0x6010b8 <rearranged+24>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
4 Arrays of Structs
有结构体如下:
struct pair {
int large;
char small;
};
struct pair pairs [2] = {
{0 xabababab , 0x1},
{0 xcdcdcdcd , 0x2}
};
每个pair
应该是 8 个字节。因为int
类型占 4 个字节,char
类型占 1 个字节。此外结构体的长度必须是结构体中体积最大的类型的整数倍,该结构体中最大的是int
类型,4 个字节,因此结构体的长度必须是 4 个字节的倍数,因此是 8 个字节,需要在char
后再加 3 个字节的padding
。pairs
是一个包含两个结构体变量的数组,1 个变量是 8 个字节,2 个就是 16 个字节。
我们验证一下(此处pairs
也可以替换成&pairs
):
(gdb) x/16bx pairs
0x601080 <pairs>: 0xab 0xab 0xab 0xab 0x01 0x00 0x00 0x00
0x601088 <pairs+8>: 0xcd 0xcd 0xcd 0xcd 0x02 0x00 0x00 0x00
此外,结构体中可以包含数组,此时结构体的alignment = max(数组中最大元素,结构体其他元素)
。例如:
struct triple {
short large [2];
char small;
};
其中short
类型 2 个字节,char
类型 1 个字节。该结构体的长度 = 2 字节的整数倍。
5 2-D Arrays
一个嵌套的数组如下:
int8_t nested [2][3] = {{0x00 , 0x01 , 0x02}, {0x10 , 0x11 , 0x12 }};
输入如下命令查看内存nested
处的值:
(gdb) x/6bx nested
0x601076 <nested>: 0x00 0x01 0x02 0x10 0x11 0x12
可以看到一个单元是 1 个字节,一共 6 个单元。数组元素按照行顺序排列。
函数access
的 C 版本如下:
int8_t access(int8_t (* arr)[3], size_t row, size_t column) {
return arr[row][column];
}
仔细查了一下资料,在 C 语言中这里有两种写法要注意一下区分:
int (*arr)[10]; // 定义一个指针arr,指向一个包含 10 个元素的数组,arr 可以是 new int[n][3], 它可以是个二维数组
int* arr[10]; // 定义一个包含 10 个元素的数组,其中每个元素都是一个int类型的指针,其起始地址为 arr
这样声明的原因是:[]
运算符的优先级比*
高,因此需要通过()
来把*arr
括起来。
由此,access
函数的传入参数arr
是一个指针,该指针指向一个 3 个元素的数组。可以有n
个这样的指针,构成一个二维数组。如arr = new int[n][3]
,那么arr
有 n 行,每行都是一个指向三个元素的数组。这种表示不能用于第二维度(列)不等于 3 的数组,如int flipped [3][2]
。
如果要将 C 语言版本的access
转化为汇编指令,&arr[row][col] = arr + row * 3 + col
(因为这里每个int
都是一个字节,因此不需要乘上多余的sizeof(T)
,否则就需要乘上sizeof(T)
。可以使用一个寄存器作为arr
,计算3 * row
,再加上col
,最后对指针解引,放到返回值中。
对函数access
进行反汇编,得到如下指令:
(gdb) disassemble access
Dump of assembler code for function access:
0x00000000004005b5 <+0>: lea (%rsi,%rsi,2),%rax ;; rax = 3 * rsi
0x00000000004005b9 <+4>: add %rax,%rdi ;; rdi += 3 * rsi
0x00000000004005bc <+7>: movzbl (%rdi,%rdx,1),%eax ;; eax = *(rdi + 3 * rsi + rdx)
0x00000000004005c0 <+11>: retq
End of assembler dump.
这里我想%rdi
是arr
的基地址,%rsi
是row
,rdx
是col
。我们首先计算了3 * rsi
,将其加到%rdi
中,然后计算3 * %rsi + %rdx
。
现在给 3 个数组:
int8_t first [3] = {0x00 , 0x01 , 0x02 };
int8_t second [3] = {0x10 , 0x11 , 0x12 };
int8_t * multilevel [2] = {first , second };
这里multilevel
是一个嵌套的数组,其第一个元素first
是一个 3 个字节的数组,其本身是个指针。第二个元素second
同理。
multilevel
的每个元素是 8 个字节。数组的每个元素是 1 个字节。输入如下指令检验:
(gdb) x/2gx multilevel
0x601060 <multilevel>: 0x0000000000601073 0x0000000000601070
(gdb) x/3bx first
0x601073 <first>: 0x00 0x01 0x02
(gdb) x/3bx multilevel[0]
0x601073 <first>: 0x00 0x01 0x02
(gdb) p &first
$4 = (int8_t (*)[3]) 0x601073 <first>
(gdb) p &second
$5 = (int8_t (*)[3]) 0x601070 <second>
将上述 C 语言程序换成如下程序:
int8_t accessMultilevel (int8_t **arr , size_t row , size_t column) {
return arr[row][column];
}
这里将int8_t (*arr)[3]
换成了int8_t **arr
,也就是说这个arr
可以指向一个二维数组,而这个二维数组的长和宽可以不定。
这里的计算公式就是a[row][col] = *(*(arr + row * 8) + column)
(因为int8_t
是 1 个字节,所以无需乘上sizeof(T)
)。此处arr
应该是基地址,row
存储在一个寄存器中,column
存储在一个寄存器中。
对该函数进行反汇编,结果如下:
(gdb) disassemble accessMultilevel
Dump of assembler code for function accessMultilevel:
0x00000000004005c1 <+0>: add (%rdi,%rsi,8),%rdx ;; %rdx += *(%rdi + 8 * %rsi)
0x00000000004005c5 <+4>: movzbl (%rdx),%eax ;; %eax = *(%rdx)
0x00000000004005c8 <+7>: retq
End of assembler dump.
这里%rdx
中存储的是column
,%rdi
中存储的是arr
的基地址,%rsi
中存储的是row
。如果first
和second
都含有 4 个元素的话,地址的计算不影响。但是如果俩数组长度不一样,那么索引的时候可能会出现地址越界的问题。
如果数组这样定义:
int8_t * multilevel [2] = {first, first};
那么如果修改一个first
的元素值的话,两个索引指向同一个数组,两索引对应数组那个值都会改变。
6 Endianness (Optional)
还是之前那个courses
结构体,我们先按照 4 个字节一组的单位打印它的值看看,再按照 1 个字节一组为单位打印前 4 个字节看看:
(gdb) x/4wx courses
0x601110 <courses>: 0x00015213 0x00015513 0x00018213 0x00018600
(gdb) x/4bx courses
0x601110 <courses>: 0x13 0x52 0x01 0x00
我们可以看到一个数(int
类型,4 个字节),按照 4 个字节一组打印的时候,显示的是0x15213
,但是按照单个字节打印的时候发现低地址字节存放在低地址处,高地址字节存放在高地址处,因此该机器为小端序机器。小端序机器的缺陷在于不方便单字节读取,字节的顺序是倒过来的。
但是小端序机器也有优点,narrowingCast
函数的 C 语言版本如下:
int narrowingCast (long *num) {
return (int) *num;
}
我们对narrowingCast
函数进行反汇编,得到如下结果:
(gdb) disassemble narrowingCast
Dump of assembler code for function narrowingCast:
0x00000000004005c9 <+0>: mov (%rdi),%eax
0x00000000004005cb <+2>: retq
End of assembler dump.
这个函数的功能是,将一个long*
类型的指针解引,然后把它指向的地址的 4 个字节赋给%eax
。
这里的一个问题是,对于一个数字:0x0000000012345678
。如果是小端法,低位字节存储在%rsp
处,一直到%rsp + 3
。而如果是大端法,低位字节存储在%rsp + 7
,高位字节存储在%rsp + 4
。因此如果是大端法,就没法用mov (%rdi),%eax
来获取值,而应该用mov 4(%rdi),%eax
来获取值