杂项
------ 绝大部分内容都来自B站绿导师操作系统课程和ChatGPT
RTFSC:Read The Fucking Source Code
RTFM:Read The Fucking Manual
STFW:Search The Fucking Web
Tools
strace
可以查看程序运行时期的系统调用和信号
比如我们想看gcc编译a.out的整个系统调用,那么就如下面这样写
strace gcc a.out
X86架构中的寄存器
cs程序计数器寄存器
在ARM架构中,程序计数器一般名为PC(Program Counter),而在X86架构中,程序计数器称为代码段寄存器,CS(Code Segment Register)
ds数据段寄存器
在 x86 架构的计算机系统中,DS 寄存器是数据段寄存器(Data Segment Register)之一。DS 寄存器用于存储当前数据段的起始地址。数据段是用于存储程序数据(变量、数组等)的内存区域。
DS 寄存器是分段机制中的一个关键寄存器,用于计算数据的物理地址。当程序需要读取或写入内存中的数据时,DS 寄存器中的值与偏移量(即数据的相对地址)结合,可以计算出数据的真实物理地址。
例如,当访问一个变量时,可以使用 DS 寄存器中存储的数据段起始地址加上变量的偏移量来计算出变量在内存中的位置。
rip寄存器
在 x86 架构的计算机系统中,RIP(Instruction Pointer)寄存器是一个特殊的寄存器,用于存储下一条将要执行的指令的地址。
RIP 寄存器是在 64 位模式下引入的,它取代了 32 位模式下的 EIP 寄存器。RIP 寄存器存储了当前指令的地址,当一条指令执行完毕后,RIP 寄存器会自动递增,指向下一条要执行的指令。
RIP 寄存器在程序执行期间不可直接修改,它由处理器自动控制和管理。指令的跳转、函数调用、条件分支等操作都会影响 RIP 寄存器的值,从而决定程序执行的流程。
通过 RIP 寄存器,处理器能够实现顺序执行、无条件跳转、条件分支和函数调用等各种控制流操作。
总结起来,RIP 寄存器在 x86 架构中用于存储下一条将要执行的指令的地址,通过自动递增和跳转等操作,控制程序的执行流程。
rsp寄存器
栈顶指针,指向栈的顶部,而
rbp指向栈基位置
rbp
rbp寄存器是栈基指针(Stack Base Pointer),也称为帧指针(Frame Pointer)。它通常用于函数调用时的栈帧设置,指向当前函数的基准位置,即栈帧的底部。通过rbp寄存器,可以访问函数的参数、局部变量和函数返回地址等。
eax寄存器
dl寄存器
rax寄存器(64位)----eax是32位上的
是64位寄存器,用于存储函数返回值、计算结果以及临时数据
函数返回值:在函数调用结束后,返回值通常会存储在
rax寄存器中。对于64位整数、指针或引用类型的返回值,会直接存储在rax寄存器中。 算术运算:
rax寄存器可以用于存储算术运算的结果,例如加法、减法、乘法和除法操作的结果。 数据传递:
rax寄存器可以用于临时存储数据,以便在不同指令之间进行传递和处理。
汇编
.byte
当你在汇编语言中使用 .byte 指令将字节数据插入到数据段中后,你可以使用标签来引用该数据,并通过相应的指令来访问它。
以下是一个示例,展示如何访问使用 .byte 指令定义的字节数据:
.section .data
my_data:
.byte 0x0A, 0x0B, 'A'
.section .text
.globl _start
_start:
; 读取数据
mov al, [my_data] ; 将 my_data 地址处的字节读取到 AL 寄存器中
mov dl, [my_data+1] ; 将 my_data 地址后一字节处的字节读取到 DL 寄存器中
; 写入数据
mov [my_data+2], dl ; 将 DL 寄存器中的字节写入 my_data 地址后两字节处的位置
; 其他操作...
exit:
mov eax, 1 ; exit 系统调用号
xor ebx, ebx ; 退出状态码
int 0x80 ; 触发系统调用
在这个示例中,首先在数据段中使用 .byte 指令定义了一个名为 my_data 的标签,并插入了字节数据 0x0A、0x0B 和字符 'A'。
在代码段中,我们使用 mov 指令来访问这些数据。例如,mov al, [my_data] 将 my_data 地址处的字节值读取到 AL 寄存器中,mov dl, [my_data+1] 将 my_data 地址后一字节处的字节值读取到 DL 寄存器中。
此外,示例中还展示了如何通过 mov [my_data+2], dl 将 DL 寄存器中的字节值写入到 my_data 地址后两字节处的位置。
注意:在访问数据时,要确保对应的数据类型和操作符的正确匹配,以避免数据访问错误。此外,具体的内存访问方式可能因汇编语言和平台而异,以上示例基于x86架构。
注意:[ ]是解引用,上面例子中my_data是一个内存中的地址,然后通过[ ]就可以得到my_data中的值
(gbd) p/x $cs * 16 + $rip
在 x86 架构中,CS(Code Segment)寄存器中存储的是代码段选择子,而不是实际的线性地址。代码段选择子用于在全局描述符表(GDT)或局部描述符表(LDT)中找到与之对应的描述符,从而确定代码段的基地址和其他属性。
选择子在 GDT 或 LDT 中的偏移量是以字节为单位的,而指令的地址是以字节为粒度的。因此,要将选择子转换为线性地址,需要乘以选择子的粒度。在实模式下,选择子的粒度为 16,即每个选择子代表 16 字节。
通过将 CS 寄存器的值乘以 16,可以将选择子转换为代码段的基地址,从而与 RIP 寄存器中的偏移量相加,得到指令的线性地址。
因此,在
(gdb) p/x $cs * 16 + $rip这个 GDB 命令中,乘以 16 的目的是将选择子转换为代码段的基地址,以便与 RIP 寄存器的偏移量相加,计算出当前指令的线性地址。最终结果以十六进制形式进行打印。
callq
是x86汇编语言中的指令,用于进行函数调用。
在x86汇编中,
callq指令的作用是将当前指令的下一条指令地址(返回地址)压入栈中,并跳转到指定的函数入口地址执行callq <function_label>
<function_label>是要调用的函数的标签或地址
xor
异或
0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1)
ORR
指令将源操作数和第二个操作数进行按位或运算,并将结果存储到目标寄存器中
EOR(Exclusive OR)
执行按位异或操作。将源操作数和目标操作数进行按位异或,并将结果存储到目标操作数中。
BL(Branch and Link)
用于进行函数调用。它将当前指令的下一条指令地址保存到链接寄存器(LR)中,并跳转到目标地址执行。
TST(Test)
执行位测试操作。对两个操作数进行按位与操作,并根据结果设置条件标志位,但不存储结果。
BIC(Bit Clear)
执行位清除操作。对目标操作数和源操作数进行按位与操作,并将结果存储到目标操作数中,清除指定位置上的位。
BNE(Branch if Not Equal)
在不相等时跳转。如果前一次算术或逻辑操作导致条件码中的"不等于"标志位设置,则执行跳转到目标地址。
使用ARM汇编实现将内存加载到寄存器R1,将其加1,然后写回到内存中
.section .data
value: .word 42 // 内存中的值
.section .text
.global _start
_start:
// 将内存加载到寄存器R1
ldr r1, =value @=表示非立即数
// 加载内存中的值到寄存器R2
ldr r2, [r1]
// 将寄存器R2中的值加1
add r2, r2, #1
// 将加1后的值写回内存
str r2, [r1]
// 退出程序
mov r7, #1 // 系统调用号1表示退出程序 r7中一般存放系统调用号
mov r0, #0 // 退出状态码为0
swi 0 // 执行系统调用,swi为软中断 0为中断号
底层
BIOS 中断向量表
| 中断 | 描述 |
|---|---|
| INT 00h | CPU:除零错,或商不合法时触发 |
| INT 01h | CPU:单步陷阱,TF标记为打开状态时,每条指令执行后触发 |
| INT 02h | CPU:非可屏蔽中断,如引导自我测试时发生内存错误。 |
| INT 03h | CPU:第一个未定义的中断向量,约定俗成仅用于调试程序 |
| INT 04h | CPU:算数溢出。通常由INTO指令在置溢出位时触发。 |
| INT 05h | 在按下Shift-Print Screen或BOUND指令检测到范围异常时触发。 |
| INT 06h | CPU:非法指令。 |
| INT 07h | CPU:没有数学协处理器时尝试执行浮点指令触发。 |
| INT 08h | IRQ0:可编程中断控制器每 55 毫秒触发一次,即每秒 18.2 次。 |
| INT 09h | IRQ1:每次键盘按下、按住、释放。 |
| INT 0Ah | IRQ2: |
| INT 0Bh | IRQ3:COM2/COM4。 |
| INT 0Ch | IRQ4:COM1/COM3。 |
| INT 0Dh | IRQ5:硬盘控制器(PC/XT 下)或 LPT2。 |
| INT 0Eh | IRQ6:需要时由软盘控制器调用。 |
| INT 0Fh | IRQ7:LPT1。 |
| INT 10h | 显示服务 - 由BIOS或操作系统设定以供软件调用。AH=00h设定显示模式AH=01h设定游标形态AH=02h设置光标位置AH=03h获取光标位置与形态AH=04h获取光标位置AH=05h设置显示页AH=06h清除或滚动栏画面(上)AH=07h清除或滚动栏画面(下)AH=08h读取游标处字符与属性AH=09h更改游标处字符与属性AH=0Ah更改游标处字符AH=0Bh设定边界颜色AH=0Eh在TTY模式下写字符AH=0Fh获取当前显示模式AH=13h写字符串 |
| INT 11h | 返回设备列表。 |
| INT 12h | 获取常规内存容量。 |
| INT 13h | 低级磁盘服务。AH=00h复位磁盘驱动器。AH=01h检查磁盘驱动器状态。AH=02h读扇区。AH=03h写扇区。AH=04h校验扇区。AH=05h格式化磁道。AH=08h获取驱动器参数。AH=09h初始化硬盘驱动器参数。AH=0Ch寻道。AH=0Dh复位硬盘控制器。AH=15h获取驱动器类型。AH=16h获取软驱中盘片的状态。 |
| INT 14h | 串口通信例程。AH=00h初始化串口。AH=01h写出字符。AH=02h读入字符。AH=03h状态。 |
| INT 15h | 其它(系统支持例程)。AH=4FH键盘拦截。AH=83H事件等待。AH=84H读游戏杆。AH=85HSysRq 键。AH=86H等待。AH=87H块移动。AH=88H获取扩展内存容量。AH=C0H获取系统参数。AH=C1H获取扩展 BIOS 数据区块。AH=C2H指针设备功能。AH=E8h, AL=01h (AX = E801h)获取扩展内存容量(自从 1994 年引入的新功能),可获取到 64MB 以上的内存容量。AH=E8h, AL=20h (AX = E820h)查询系统地址映射。该功能取代了 AX=E801h 和 AH=88h。 |
| INT 16h | 键盘通信例程。AH=00h读字符。AH=01h读输入状态。AH=02h读 Shift 键(修改键)状态。AH=10h读字符(增强版)。AH=11h读输入状态(增强版)。AH=12h读 Shift 键(修改键)状态(增强版)。 |
| INT 17h | 打印服务。AH=00h打印字符。AH=01h初始化打印机。AH=02h检查打印机状态。 |
| INT 18h | 执行磁带上的 BASIC 程序:“真正的”IBM 兼容机在 ROM 里内置 BASIC 程序,当引导失败时由 BIOS 调用此例程解释执行。(例:打印“Boot disk error. Replace disk and press any key to continue…”这类提示信息) |
| INT 19h | 加电自检之后加载操作系统。 |
| INT 1Ah | 实时钟服务。AH=00h读取实时钟。AH=01h设置实时钟。AH=02h读取实时钟时间。AH=03h设置实时钟时间。AH=04h读取实时钟日期。AH=05h设置实时钟日期。AH=06h设置实时钟闹铃。AH=07h重置实时钟闹铃。 |
| INT 1Bh | Ctrl+Break,由 IRQ 9 自动调用。 |
| INT 1Ch | 预留,由 IRQ 8 自动调用。 |
| INT 1Dh | 不可调用:指向视频参数表(包含视频模式的数据)的指针。 |
| INT 1Eh | 不可调用:指向软盘模式表(包含关于软驱的大量信息)的指针。 |
| INT 1Fh | 不可调用:指向视频图形字符表(包含从 80h 到 FFh 的 ASCII 字符的数据)的信息。 |
| INT 41h | 地址指针:硬盘参数表(第一硬盘)。 |
| INT 46h | 地址指针:硬盘参数表(第二硬盘)。 |
| INT 4Ah | 实时钟在闹铃时调用。 |
| INT 70h | IRQ8:由实时钟调用。 |
| INT 74h | IRQ12:由鼠标调用 |
| INT 75h | IRQ13:由数学协处理器调用。 |
| INT 76h | IRQ14:由第一个 IDE 控制器所调用 |
| INT 77h | IRQ15:由第二个 IDE 控制器所调用 |
GDB调试
在 GDB(GNU Debugger)中进行调试的基本流程如下:
编译程序时加入调试信息:在编译程序时,确保添加
-g参数以包含调试信息。例如,使用gcc编译 C 代码时可以执行gcc -g program.c -o program。启动 GDB:在命令行中输入
gdb后跟上可执行文件的路径,如gdb program。设置断点:在 GDB 的命令行界面中,使用
break命令设置断点。可以使用函数名、行号或地址作为断点位置。例如,break main、break 10、break *0x4005a0。启动程序:在 GDB 中,输入
run命令或简写为r,然后按回车键启动程序。程序将开始执行,直到遇到设置的断点或程序结束。执行调试操作:一旦程序停在断点处,你可以执行多种调试操作,如查看变量的值、单步执行、查看堆栈、打印信息等。以下是一些常用的调试命令:
list或l:显示源代码。p:打印变量的值。step或s:单步执行,进入函数内部。next或n:单步执行,跳过函数内部。continue或c:继续执行程序直到下一个断点。backtrace或bt:显示函数调用堆栈。quit或q:退出 GDB。分析问题和调试结果:通过观察变量的值、程序执行流程和堆栈信息,分析程序的行为并找出问题所在。
qemu-system模拟器使用GDB调试
使用
qemu-system -S -s 可以进行远程调试通过查
man qemu-system可以看到-s表示连接到tcp:1234端口,然后进入gbd中,使用target remote localhost:1234即可连接GBD和模拟器
GBD调试
-
选项-p是 打印变量信息
-
starti,从程序的第一条指令开始执行 -
info register查看寄存器的信息(注意,要在程序运行的时候才可以查看) -
x $rsp,dereference rsp,就是解引用,查看寄存器中存放地址的值
layout
在 GDB(GNU Debugger)中,
layout命令用于控制调试界面的布局,以便更好地查看代码、堆栈和其他相关信息。
layout命令有多种选项,可以根据需要选择不同的布局:
layout src:以源代码布局显示调试界面。这种布局在当前行周围显示源代码,并可以高亮显示当前执行的行。
layout asm:以汇编代码布局显示调试界面。这种布局在当前行周围显示反汇编指令,可以查看程序的底层执行。
layout reg:以寄存器布局显示调试界面。这种布局显示当前 CPU 寄存器的值,包括通用寄存器、特殊寄存器和标志寄存器。
layout split:以源代码和汇编代码并排的布局显示调试界面。这种布局在左侧显示源代码,右侧显示反汇编指令。
layout next:切换到下一个布局。每次调用layout next命令,GDB 将切换到下一个可用的布局选项。
layout prev:切换到上一个布局。每次调用layout prev命令,GDB 将切换到上一个可用的布局选项。可以根据需要在 GDB 的命令行界面中使用
layout命令切换不同的布局。这些布局选项可以帮助你更方便地查看和理解程序的执行状态,以及相关的源代码和汇编指令。
gdb -x
在 GDB(GNU Debugger)中,
-x是一个命令行选项,用于指定一个 GDB 脚本文件来自动执行调试命令。通过
-x选项,你可以在启动 GDB 时指定一个包含调试命令的脚本文件,这些命令将自动执行,而无需手动在 GDB 命令行中输入。这对于自动化调试过程或批处理调试非常有用。使用
-x选项的语法如下:gdb -x script_file其中,
script_file是包含 GDB 命令的文本文件的路径。在脚本文件中,你可以按照 GDB 的命令语法编写一系列调试命令,每个命令占据一行。GDB 将按照脚本文件中命令的顺序执行这些命令。这使得你可以事先定义好一系列需要执行的调试操作,而无需手动逐个输入命令。
以下是一个示例脚本文件的内容:
break main run print variable next上述脚本文件将在程序的
main函数处设置断点,然后执行程序,打印一个名为variable的变量的值,并单步执行到下一行。要使用
-x选项执行脚本文件,你可以在命令行中输入类似下面的命令:gdb -x myscript.gdb myprogram上述命令将启动 GDB,并使用名为
myscript.gdb的脚本文件执行调试命令,同时调试名为myprogram的可执行文件。使用
-x选项可以方便地批量执行预定义的调试命令,提高调试效率和自动化程度。
C++代码
what is the - - > operator in C/C++
#include <stdio.h> int main() { int x = 10; while (x --> 0) // x goes to 0 { printf("%d ", x); } }输出结果是
9 8 7 6 5 4 3 2 1 0
C语言
#pragma
c语言中修改字节对齐大小可以是用
#pragma pack(int)去设置字节对齐大小以下内容来自ChatGPT
#pragma是C语言中的一个预处理指令,用于向编译器提供特定的指示或指令。它是一个编译器特定的指令,并且不是C语言标准的一部分。#pragma指令的具体行为和效果取决于编译器的实现。
#pragma指令可以用于各种目的,例如:
控制编译器的警告和错误信息:
#pragma warning(disable: 1234)这个指令告诉编译器禁用特定警告代码为1234的警告信息。
控制编译器的优化行为:
#pragma optimize("s", on)这个指令告诉编译器在编译过程中进行优化,以提高代码的执行速度。
控制编译器的对齐方式:
#pragma pack(1)这个指令告诉编译器按照字节对齐方式设置结构体的对齐方式。
请注意,
#pragma指令的行为和支持的选项因编译器而异。不同的编译器可能支持不同的#pragma指令,或者对相同的指令有不同的行为。因此,在使用#pragma指令时,最好查阅特定编译器的文档以了解其支持和行为。
Make
make -nB
make -nB是一个命令行选项,用于在执行构建过程之前打印构建命令,而不实际执行它们。这个选项通常用于查看构建系统生成的命令,以便了解构建过程中会执行哪些操作。具体而言,
-n选项告诉Make不要实际执行命令,而是打印出将要执行的命令。-B选项则表示强制执行构建,即使目标文件已经是最新的。当你在命令行中运行
make -nB时,它会解析Makefile或CMakeLists.txt文件,确定需要构建的目标以及相关的依赖关系。然后,它会打印出每个目标所对应的构建命令,而不实际执行这些命令。这样你就可以预览构建过程中会执行的操作,但并不会真正进行构建。这种预览构建命令的方式对于调试构建脚本、检查依赖关系、验证构建过程是否正确配置等非常有用。一旦你确认构建命令的正确性,可以去掉
-n选项,以实际执行构建过程。
vim
:%s/ /\r /g
在Vim中,
:%s/ /\r /g是一个替换命令,用于在当前打开的文档中将空格(空格字符)替换为换行符。具体解释如下:
%表示对整个文档进行操作。s/ /是替换命令的开始。它指示将后面的空格替换为另一个内容。\r是一个特殊的转义序列,表示换行符(Carriage Return)。/g是替换命令的标志,表示在每一行上执行全局替换,而不仅仅是第一个匹配。因此,该命令的作用是将当前文档中的所有空格替换为换行符。换行符将会在每个空格位置处插入,从而将文本分割成多行,每个单词或单词片段都将独占一行。
注意:执行该命令将修改当前打开的文档,如果不希望修改文档,可以先使用
:setl modifiable命令使文档可编辑,执行完替换后再使用:setl nomodifiable命令将文档设为不可编辑。
:set nowrap
在Vim中,
:set nowrap命令用于关闭自动换行功能,使文本在一行内水平滚动,而不是自动换行到下一行。
当你执行:set nowrap后,Vim将禁用自动换行功能。这意味着无论文本的长度如何,它都将在当前行内显示,并且你需要使用水平滚动来查看超出屏幕宽度的内容。
这个命令在处理较长的代码行或查看不需要自动换行的文本时非常有用。如果你想恢复自动换行,可以使用命令:set wrap。
:nohl
清除高亮,当使用了搜索后(例如使用
/或?命令进行搜索),就会出现高亮,然后使用:nohl就可以清除
我通过gcc -Wl 打印的信息怎么输出到vim中
保留需要的,删除不需要的内容
:%!grep 需要保留的信息 x :%!grep execve
那这么同时保留多个呢,可以使用
-e:%!grep -e execve -e open
反向过滤不需要的信息 -v
:%!grep -v 不想看的信息
:'<,'>!grep 1
在 Vim 中,
'<,'>是一个标识符,表示当前选定的文本范围。它表示从当前选定的起始行到结束行的文本范围。
!grep 1是 Vim 中的外部命令调用语法,用于在当前选定的文本范围内执行grep 1命令。具体来说,它会将选定的文本作为输入传递给grep 1命令,并将匹配到的结果显示在 Vim 缓冲区中。因此,
'<,'>!grep 1的作用是在当前选定的文本范围内执行grep 1命令,并将匹配到的结果显示在 Vim 缓冲区中。这样可以方便地在选定的文本中查找特定模式并显示匹配结果。会把所有有1的数据显示出来过滤掉不含1的,
grep 1类似于上一个内容
GCC
gcc -Wl,--verbose *.o *.o
在 GCC 编译器中,
-Wl,--verbose是 一个参数组合,-Wl用于向链接器传递--verbose选项。该选项会让链接器以详细模式运行,并输出更多关于链接过程的信息。通过使用
-Wl,--verbose,你可以获取链接器的详细输出,包括搜索路径、链接的库文件和符号解析等信息。这对于调试链接错误或了解链接器的操作非常有用。
asm
在 Linux 中,“asm” 文件夹通常用于存放汇编语言源代码文件或相关的编译器头文件。这个文件夹中的文件通常包含与底层硬件交互或进行低级别编程相关的代码。
“asm” 是 “assembly” 的缩写,代表汇编语言。汇编语言是一种低级别的编程语言,与特定的硬件体系结构密切相关。在 “asm” 文件夹中,你可能会找到以下类型的文件:
- 汇编源代码文件(例如
.s或.asm后缀的文件):这些文件包含了直接与底层硬件交互的汇编指令,用于编写底层的系统软件或设备驱动程序。- 汇编编译器的头文件:这些文件包含了与汇编语言相关的常量、宏定义、寄存器描述等信息,可以在汇编源代码中使用。
“asm” 文件夹在 Linux 系统中通常作为一个特定功能或组件的一部分存在,例如内核源代码中的 “asm” 文件夹用于存放与体系结构相关的汇编代码。不同的软件或项目可能会将 “asm” 文件夹用于不同的目的,具体用途取决于软件的设计和需求。
不加-o指定输出名称,默认为a.out,这个a是什么
assembler的缩写
编译为静态文件
gcc app.c -static
linux内核
内核内存分配
linux中,内核分配主要有两个函数kmalloc,vmalloc。那他们有什么区别呢?
kmalloc
这个函数会分配一个虚拟平台连续的内存和物理内存上连续的空间。还有一点,内核为了满足任意数量的内存大小使用,创建了一些一定大小的空间,当使用kmalloc去分配空间的时候,他就会把大小接近的空间分配出来。
在4k字节大小的页系统上,分配空间的最小内存是32字节,最大是128k字节
vmalloc
这个函数会分配一个虚拟平台连续的内存而物理内存上不一定连续的空间。
补充:kzalloc
kzalloc是Linux内核中的一个函数,用于分配并初始化内核内存。它是kmalloc和memset的结合。首先它会分配一定数量的内核内存,然后将分配的内存区域设置为零。它通常用于当你需要一个全新的、内容全为零的内存块时。这是
kzalloc的函数原型:void *kzalloc(size_t size, gfp_t flags);参数:
size:需要分配的内存块的大小,以字节为单位。flags:分配标记,这些标记可以影响内存的分配行为。例如,一个常见的标记是GFP_KERNEL,它告诉内核在需要时可以将其他内存页面交换到磁盘上以分配更多的内存。返回值:
- 成功时,返回一个指向新分配和初始化的内存的指针。
- 如果无法分配所请求的内存量,则返回
NULL。以下是一个简单的使用例子:
struct my_struct *p; p = kzalloc(sizeof(*p), GFP_KERNEL); if (!p) return -ENOMEM; /* 'p' is now a pointer to a zero-initialized block of memory */这将分配一个
my_struct大小的内存块,并将其所有位设置为零。请注意检查kzalloc的返回值,以确保内存分配成功。
