提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
C语言的结果和数组多种初始化的效果,以下内容默认是64位CPU指令集,除非有特殊说明。
提示:以下是本篇文章正文内容,下面案例可供参考
一、结构体初始化
#include <string.h>
#include <stdio.h>
typedef struct
{
char a;
int b;
short c;
}UnAlignment_T;
int main(void)
{
UnAlignment_T test = {0};
printf(" test\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n", &test.a, &test.b, &test.c);
UnAlignment_T mem;
printf(" mem\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n", &mem.a, &mem.b, &mem.c);
memset((void*)&mem, 0xff, sizeof(UnAlignment_T));
printf(".a = %d\n.b=%d\n.c=%d\n", mem.a, mem.b, mem.c);
return 0;
}
我们经常有疑问:test = {0}的初始化效果是什么,是只给第一个成员变量a初始化为0,还是全部都初始化为0?从实验的答案来看,是把所有内存都初始化为0 |
查看汇编语言,就能看到初始化的效果:
.file "init.c"
.section .rodata
.align 8
.LC0:
.string " test\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n"
.align 8
.LC1:
.string " mem\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n"
.LC2:
.string ".a = %d\n.b=%d\n.c=%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movq $0, -16(%rbp)
movl $0, -8(%rbp)
leaq -16(%rbp), %rax
leaq 8(%rax), %rcx
leaq -16(%rbp), %rax
leaq 4(%rax), %rdx
leaq -16(%rbp), %rax
movq %rax, %rsi
movl $.LC0, %edi
movl $0, %eax
call printf
leaq -32(%rbp), %rax
leaq 8(%rax), %rcx
leaq -32(%rbp), %rax
leaq 4(%rax), %rdx
leaq -32(%rbp), %rax
movq %rax, %rsi
movl $.LC1, %edi
movl $0, %eax
call printf
leaq -32(%rbp), %rax
movl $12, %edx
movl $255, %esi
movq %rax, %rdi
call memset
movzwl -24(%rbp), %eax
movswl %ax, %ecx
movl -28(%rbp), %edx
movzbl -32(%rbp), %eax
movsbl %al, %eax
movl %eax, %esi
movl $.LC2, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
.section .note.GNU-stack,"",@progbits
调试标签:
.cfi_def_cfa_offset:
cfi:Call Frame Information
cfa:canonical Frame Address
.cfi_def_cfa_offset modifies a rule for computing CFA. Register remains the same, but offset is new. Note that it is the absolute offset that will be added to a defined register to compute CFA address.
以下语句进入main函数
pushq %rbp # 将栈底入栈
movq %rsp, %rbp # 栈底移动到当前栈顶
subq $32, %rsp # 栈顶向低地址移动32个字节,test + mem
以下是初始化语句,可以看到实际上只初始化了12个字节,但是却占了16个字节。
movq $0, -16(%rbp) # 为了不移动栈顶,所以使用rbp?
movl $0, -8(%rbp)
打印test的成员变量a、b、c的地址如下:
test
.a address = 0xf98b0e90
.b address = 0xf98b0e94
.c address = 0xf98b0e98
mem
.a address = 0xf98b0e80
.b address = 0xf98b0e84
.c address = 0xf98b0e88
从以上地址可以得出以下结论:
结构体入栈的顺序是test的c、b、a,mem的c、b、a
test结构体初始化示意图如下:
灰色部分未初始化,c更靠近栈底,a更靠近栈顶。按照int宽度,操作系统对所有成员进行默认对齐。
作为对比,我们可以看一下memset是如何初始化的:
leaq -32(%rbp), %rax # %rax指向mem的首地址
movl $12, %edx # %edx等于12,为结构体字节数,作为memset的第3个参数
movl $255, %esi # %esi等于0xff,作为memset的第2个参数
movq %rax, %rdi # %rdi指向mem的首地址,作为memset的第1个参数
call memset # 执行C LIB函数调用
从上述操作来看,memset的性能远不如,= {0}。 |
---|
有关打印等相关说明见第三章
二、数组初始化
2.1 定义初始化
#include <string.h>
int main(void)
{
int array_equalZero[1000] = {0};
int array_memset[1000];
memset((void*)&array_memset[0], 0, sizeof(array_memset));
return 0;
}
对应汇编语言:
.file "arrayInit.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $8000, %rsp
leaq -4000(%rbp), %rsi
movl $0, %eax
movl $500, %edx
movq %rsi, %rdi
movq %rdx, %rcx
rep stosq
leaq -8000(%rbp), %rax
movl $4000, %edx
movl $0, %esi
movq %rax, %rdi
call memset
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
.section .note.GNU-stack,"",@progbits
进入main函数:
pushq %rbp
movq %rsp, %rbp
将rsp移动至栈顶,2个1000成员的int数组。8000字节
subq $8000, %rsp
执行array_equalZero[1000]= {0}初始化。可以看到执行了500次循环完成初始化。
leaq -4000(%rbp), %rsi # %rsi指向array_equalZero首地址,更靠近栈底的4000字节
movl $0, %eax # %eax等于0
movl $500, %edx # %edx等于500
movq %rsi, %rdi # %rdi指向array_equalZero首地址
movq %rdx, %rcx # %rcx保存循环次数,赋值为500,因为要执行stosq,所以要用64位寄存器
rep stosq # 循环执行500次,将%eax中的值拷贝到%rdi指向的内存中
调用memset对数组进行初始化
leaq -8000(%rbp), %rax # %rax 指向array_memset首地址
movl $4000, %edx # %edx等于4000
movl $0, %esi # %esi等于0
movq %rax, %rdi # %rdi指向array_memset首地址
call memset # 执行C LIB库函数
movl $0, %eax
leave
memset函数源码如下:
void * __cdecl memset (
void *dst,
int val,
size_t count
)
{
void *start = dst;
#if defined (_M_IA64) || defined (_M_AMD64)
{
__declspec(dllimport)
void RtlFillMemory( void *, size_t count, char );
RtlFillMemory( dst, count, (char)val );
}
#else /* defined (_M_IA64) || defined (_M_AMD64) */
while (count--) {
*(char *)dst = (char)val;
dst = (char *)dst + 1;
}
#endif /* defined (_M_IA64) || defined (_M_AMD64) */
return(start);
}
现在大家普遍使用的CPU都不是安腾系列,所以进入#else分支。
汇编实现的memset
我们还是来看一下arch/x86/boot/copy.s中的实现:
以下命令低32位CPU指令集,函数的参数寄存器依次:EBX、ECX、EDX、ESI、EDI
返回值放在EAX中。
GLOBAL(memset)
pushw %di
movw %ax, %di
movzbl %dl, %eax
imull $0x01010101,%eax
pushw %cx
shrw $2, %cx
rep; stosl
popw %cx
andw $3, %cx
rep; stosb
popw %di
retl
ENDPROC(memset)
2.2 执行过程中初始化
如果复杂的结构(包含多个结构、数组)在运行过程中需要重新初始化,就不能用={0},此时用memset会比较简单。但是对于大块的内存,memset会消耗性能。不建议进行大内存块的memset。那此时需要制定内存的初始化机制:
1、结构定义一个标志位:useFlag,表明本段内存是否使用。如果需要初始化,则useFlag=0
2、保存字符串的数组,定义一个count字段,指示当前有效字符个数,如果需要初始化,则count=0,同时将arrayString[0] = ‘\0’;
3、对于整数等普通数据数组,同样设置count字段,指示当前有效数据个数,如果需要初始化,则count=0
4、对结构体中强相关的数据进行封装,对需要初始化的内容,进行赋值初始化。因为赋值初始化可以通过movw、movl等指令进行,不需要单字节循环,也比memset效率高
三、打印说明
疑问:为什么%rdi的值要用%rax来中转一下,不能直接赋值呢? |
test各成员地址的打印操作如下(C库入参寄存器顺序:RDI,RSI,RDX,RCX,R8,R9):
leaq -16(%rbp), %rax # test先入栈,所以在更靠近栈底的16个字节,此时%rax指向a
leaq 8(%rax), %rcx # rcx等于c的地址
leaq -16(%rbp), %rax # rax等于a的地址
leaq 4(%rax), %rdx # rdx等于b的地址
leaq -16(%rbp), %rax # rax等于a的地址
movq %rax, %rsi # rsi等于a的地址
movl $.LC0, %edi # edi保存常量字符串.LC0的地址
movl $0, %eax # printf参数结束
call printf # 执行库调用
mem的成员地址打印如下,具体就不详细解释了。
leaq -32(%rbp), %rax
leaq 8(%rax), %rcx
leaq -32(%rbp), %rax
leaq 4(%rax), %rdx
leaq -32(%rbp), %rax
movq %rax, %rsi
movl $.LC1, %edi
movl $0, %eax
call printf
以下为mem成员取值打印
movzwl -24(%rbp), %eax # -24(%rbp)指向c,取出16bits数存入%eax,高位补0
movswl %ax, %ecx # 从%ax取出16bits数,将其存取%ecx,高位补符号位
movl -28(%rbp), %edx # %edx等于b
movzbl -32(%rbp), %eax # -32(%rbp)指向a,取出8bits数存入%eax,高位补0
movsbl %al, %eax # 从%al取出8bits数,将其存入%eax,高位补1
movl %eax, %esi # %esi等于a
movl $.LC2, %edi # %edi等于常量字符串.LC2
movl $0, %eax # 变长参数结束
call printf # 执行C LIB调用
疑问:是否可以直接:movswl -24(%rbp), %ecx ? |
恢复栈顶,并出栈
movl $0, %eax
leave
leave等价于(AT&T汇编leave指令.)
movl %ebp, %esp
popl %ebp