分配粒度和内存页面大小(x86处理器平台的分配粒度是64K,内存页是4K,所以section都是0x1000对齐,硬盘扇区大小是512字节,所以PE文件默认文件对齐是0x200)
分配粒度和内存页面大小
x86处理器平台的分配粒度是64K,32位CPU的内存页面大小是4K,64位是8K,保留内存地址空间总是要和分配粒度对齐。一个分配粒度里包含16个内存页面。
这是个概念,具体不用自己操心,比如用VirtualAllocEx等函数,给lpAddress参数NULL系统就会自动找一个地方分配你要的内存空间。如果需要自己管理这个就累了......
一个分配粒度是64K,这就是为什么Null指针区域和64K进入区域都是 64K的原因,刚好就是一个分配粒度。
一个内存页是4K,这就是为什么PE文件中的section都是0x1000对齐.
硬盘扇区大小是512字节,这就是为什么PE文件默认文件对齐是0x200.
这些数字绝对不是心血来潮设定出来的,而是综合了硬件结构和操作系统架构设定的。
为什么内存空间分配总是以64K为边界?
有时候你可能会有这样的疑惑:为什么页面大小是4K,而VirtualAlloc函数分配的内存是以64K(而不是4K)为边界呢?
事情还得从Alpha AXP处理器说起
在Alpha AXP处理器上,没有一条指令对应”加载一个32位的证书”这样的操作,取而代之地,实际上是加载两个16位的整数然后将它们合并成32位的。
所以,如果内存分配粒度小于64K,则一个需要重新定位的DLL将会需要对每一次重新分配的地址做出两次调整:一次是高16位地址,另一次是低16位地址。如果这改变了两半地址之间的进位或借位,情况就会变得更糟。(例如,将4K的数据从0x1234F000移到0x12350000,这将迫使地址的低16位和高15位都发生改变。即使移动的地址量远小于64K,但由于存在进位,这项操作仍然对地址的高16位部分产生影响)
除了这个,还有更糟的
Alpha AXP处理器实际上会合并两个16位的带符号整数到一个32位的整数。例如,为了加载0x1234ABCD,你需要首先使用LDAH指令加载0x1234到目标寄存器的高16位,然后再使用LDA指令加上带符号整数-0x5433(因为,0x5433 = 0x10000 – 0xABCD),才能得到预期的结果0x1234ABCD。
因此,如果重定位导致地址在64K块的”下半部分”和”上半部分”之间移动,则必须进行其他修正,以确保正确调整了地址上半部分的算法。由于编译器喜欢对指令进行重新排序,因此该LDAH指令可能距离很远,因此下半部分的重定位记录将不得不采用某种方式来找到匹配的上半部分。
而且,编译器很聪明,如果需要为同一64K区域中的两个变量计算地址,则它们之间共享LDAH指令。如果可以通过不是64K的倍数的值进行重定位,则编译器将不再能够执行此优化,因为在重定位之后,这两个变量不再属于同一64K内存块中。
强制以64K粒度分配内存可解决所有这些问题。
如果你仔细观察的话,你会发现这也解释了为什么2GB边界附近有64K的”无人区”。考虑一下计算值0x7FFFABCD的方法:由于低16位在64K范围内,该值需要通过减法而不是加法来计算。一种比较想当然的解决方案:
上面的做法行不通
Alpha AXP是64位处理器,0x8000不适合放入到16位带符号整数中,因此必须使用-0x8000(负数)。所以,实际发生的是:
你需要添加第三条指令来清除高32位。一种巧妙的技巧是,将零加起来,然后告诉处理器将结果视为32位整数并将其符号扩展为64位。
如果允许访问2GB边界的64K范围内的地址,则每一次内存地址计算都必须插入上面所提到的第三条ADDL指令,以防万一该地址被重新分配到2GB边界附近的“危险区域”。
要访问地址空间中的最后一个64K区域,会付出一笔非常高的代价:对于所有地址计算,其性能都会受到50%的影响,以避免在实践中永远不会发生这种情况。因此,将这片区域设置为永久禁用,是一种更为明智的选择。
windows虚拟内存管理
内存管理是操作系统非常重要的部分,处理器每一次的升级都会给内存管理方式带来巨大的变化,向早期的8086cpu的分段式管理,到后来的80x86 系列的32位cpu推出的保护模式和段页式管理。在应用程序中我们无时不刻不在和内存打交道,我们总在不经意间的进行堆内存和栈内存的分配释放,所以内存是我们进行程序设计必不可少的部分。
CPU的内存管理方式
段寄存器怎么消失了?
在学习8086汇编语言时经常与寄存器打交道,其中8086CPU采用的内存管理方式为分段管理的方式,寻址时采用:短地址 * 16 + 偏移地址的方式,其中有几大段寄存器比如:CS、DS、SS、ES等等,每个段的偏移地址最大为64K,这样总共能寻址到2M的内存。但是到32位CPU之后偏移地址变成了32位这样每个段就可以有4GB的内存空间,这个空间已经足够大了,这个时候在编写相应的汇编程序时我们发现没有段寄存器的身影了,是不是在32位中已经没有段寄存器了呢,答案是否定了,32位CPU中不仅有段寄存器而且它们的作用比以前更大了。 在32位CPU中段寄存器不再作为段首地址,而是作为段选择子,CPU为了管理内存,将某些连续的地址内存作为一页,利用一个数据结构来说明这页的属性,比如是否可读写,大小,起始地址等等,这个数据结构叫做段描述符,而多个段描述符则组成了一个段描述符表,而段寄存器如今是用来找到对应的段描述符的,叫做段选择子。段寄存器仍然是16位其中高13位表示段描述符表的索引,第二位是区分LDT(局部描述符表)和GDT(全局描述符表),全局描述符表是系统级的而LDT是每个进程所独有的,如果第二位表示的是LDT,那么首先要从GDT中查询到LDT所在位置,然后才根据索引找到对应的内存地址,所以现在寻址采用的是通过段选择子查表的方式得到一个32位的内存地址。由于这些表都是由系统维护,并且不允许用户访问及修改所以在普通应用程序中没有必要也不能使用段寄存器。通过上面的说明,我们可以推导出来32位机器最多可以支持2^(13 + 1 + 32) = 64T内存。
段页式管理
通过查表方式得到的32位内存地址是否就是真实的物理内存的地址呢,这个也是不一定的,这个还要看系统是否开启了段页式管理。如果没有则这个就是真实的物理地址,如果开启了段页式管理那么这个只是一个线性地址,还需要通过页表来寻址到真实的物理内存。 32位CPU专门新赠了一个CR3寄存器用来完成分页式管理,通过CR3寄存器可以寻址到页目录表,然后再将32位线性地址的高10位作为页目录表的索引,通过这个索引可以找到相应的页表,再将中间10为作为页表的索引,通过这个索引可以寻址到对应物理内存的起始地址,最后通过这个其实地址和最后低12位的偏移地址找到对应真实内存。下面是这个过程的一个示例图:

为什么要使用分页式管理,直接让那个32位线性地址对应真实的内存不可以吗。当然可以,但是分页式管理也有它自身的优点: 1. 可以实现页面的保护:系统通过设置相关属性信息来指定特权级别和其他状态 2. 可以实现物理内存的共享:从上面的图中可以看出,不同的线性地址是可以映射到相同的物理内存上的,只需要更改页表中对应的物理地址就可以实现不同的线性地址对应相同的物理内存实现内存共享。 3. 可以方便的实现虚拟内存的支持:在系统中有一个pagefile.sys的交互页面文件,这个是系统用来进行内存页面与磁盘进行交互,以应对内存不够的情况。系统为每个内存页维护了一个值,这个值表示该页面多久未被访问,当页面被访问这个值被清零,否则每过一段时间会累加一次。当这个值到达某个阈值时,系统将页面中的内容放入磁盘中,将这块内存空余出来以便保存其他数据,同时将之前的线性地址做一个标记,表名这个线性地址没有对应到具体的内存中,当程序需要再次访问这个线性地址所对应的内存时系统会再次将磁盘中的数据写入到内存中。虽说这样做相当于扩大了物理内存,但是磁盘相对于内存来说是一个慢速设备,在内存和磁盘间进行数据交换总是会耗费大量的时间,这样会拖慢程序运行,而采用SSD硬盘会显著提高系统运行效率,就在于SSD提高了与内存进行数据交换的效率。如果想显著提高效率,最好的办法是加内存毕竟在内存和硬盘间倒换数据是要话费时间的。
保护模式
在以前的16位CPU中采用的多是实模式,程序中使用的地址都是真实的物理地址,这样如果内存分配不合理,会造成一个程序将另外一个程序所在的内存覆盖这样对另外一个程序将造成严重影响,但是在32位保护模式下,不再会产生这种问题,保护模式将每个进程的地址空间隔离开来,还记得上面的LDT吗,在不同的程序中即使采用的是相同的地址,也会被LDT映射到不同的线性地址上。 保护模式主要体现在这样几个方面: 1.同一进程中,使用4个不同访问级别的内存段,对每个页面的访问属性做了相应的规定,防止错误访问的情况,同时为提供了4中不同代码特权,0特权的代码可以访问任意级别的内存,1特权能任意访问1…3级内存,但不能访问0级内存,依次类推。通常这些特权级别叫做ring0-ring3。 2. 对于不同的进程,将他们所用到的内存等资源隔离开来,一个进程的执行不会影响到另一个进程。
windows系统的内存管理
windows内存管理器
我们将系统中实际映射到具体的实际内存上的页面称为工作集。当进程想访问多余实际物理内存的内存时,系统会启用虚拟内存管理机制(工作集管理),将那些长时间未访问的物理页面复制到硬盘缓冲文件上,并释放这些物理页面,映射到虚拟空间的其它页面上;系统的内存管理器主要由下面的几个部分组成: 1. 工作集管理器(优先级16):这个主要负责记录每个页面的年龄,也就有多久未被访问,当页面被访问这个年龄被清零,否则每过一段时间就进行累加1的操作。 2. 进程/栈交换器(优先级23):主要用于在进行进程或者线程切换时保存寄存器中的相关数据用以保存相关环境。 3. 已修改页面写出器(优先级17):当内存映射的内容发生改变时将这个改变及时的写入到硬盘中,防止由于程序意外终止而造成数据丢失 4. 映射页面写出器(优先级17):当页面的年龄达到一定的阈值时,将页面内容写入到硬盘中 5. 解引用段线程(优先级18):释放以写入到硬盘中的空闲页面 6. 零页面线程(优先级0):将空闲页面清零,以便程序下次使用,这个线程保证了新提交的页面都是干净的零页面
进程虚拟地址空间的布局
windows为每个进程提供了平坦的4GB的线性地址空间,这个地址空间被分为用户分区和内核分区,他们各占2GB大小,其中内核分区在高地址位,用户分区在低地址位,下面是内存分布的一个表格:
分区 |
地址范围 |
---|---|
NULL指针区 |
0x00000000-0x0000FFFF |
用户分区 |
0x00010000-0x7FFEFFFF |
64K禁入区 |
0x7FFF0000-0x7FFFFFFF |
内核分区 |
0x80000000-0xFFFFFFFF |
从上面的图中可以看出,系统的内核分区是2GB而用户可用的分区并没有2GB,在用户分区的头64K和尾部的64K不允许用户使用。 另外我们可以压缩内核分区的大小,以便使用户分区占更多的内存,这就是/3GB方式,下面是这种方式的具体内存分布:
分区 |
地址范围 |
---|---|
NULL指针区 |
0x00000000-0x0000FFFF |
用户分区 |
0x00010000-0xBFFEFFFF |
64K禁入区 |
0xBFFF0000-0xBFFFFFFF |
内核分区 |
0xC0000000-0xFFFFFFFF |
windows虚拟内存管理函数
VirtualAlloc
VirtualAlloc函数主要用于提交或者保留一段虚拟地址空间,通过该函数提交的页面是经过0页面线程清理的干净的页面。
LPVOID VirtualAlloc(
LPVOID lpAddress, //虚拟内存的地址
DWORD dwSize, //虚拟内存大小
DWORD flAllocationType,//要对这块的虚拟内存做何种操作
DWORD flProtect //虚拟内存的保护属性
);
我们可以指定第一个参数来告知系统,我们希望操作哪块内存,如果这个地址对应的内存已经被保留了那么将向下偏移至64K的整数倍,如果这块内存已经被提交,那么地址将向下偏移至4K的整数倍,也就是说保留页面的最小粒度是64K,而提交的最小粒度是一页4K。 第三个参数是指定分配的类型,主要有以下几个值
值 |
含义 |
---|---|
MEM_COMMIT |
提交,也就是说将虚拟地址映射到对应的真实物理内存中,这样这块内存就可以正常使用 |
MEM_RESERVE |
保留,告知系统以这个地址开始到后面的dwSize大小的连续的虚拟内存程序要使用,进程其他分配内存的操作不得使用这段内存。 |
MEM_TOP_DOWN |
从高端地址保留空间(默认是从低端向高端搜索) |
MEM_LARGE_PAGES |
开启大页面的支持,默认一个页面是4K而大页面是2M(这个视具体系统而定) |
MEM_WRITE_WATCH |
开启页面写入监视,利用GetWriteWatch可以得到写入页面的统计情况,利用ResetWriteWatch可以重置起始计数 |
MEM_PHYSICAL |
用于开启PAE |
第四个参数主要是页面的保护属性,参数可取值如下:
值 |
含义 |
---|---|
PAGE_READONLY |
只读 |
PAGE_READWRITE |
可读写 |
PAGE_EXECUTE |
可执行 |
PAGE_EXECUTE_READ |
可读可执行 |
PAGE_EXECUTE_READWRITE |
可读可写可执行 |
PAGE_NOACCESS |
不可访问 |
PAGE_GUARD |
将该页设置为保护页,如果试图对该页面进行读写操作,会产生一个STATUS_GUARD_PAGE 异常 |
下面是该函数使用的几个例子: 1. 页面的提交/保留与释放
//保留并提交
LPVOID pMem = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
srand((unsigned int)time(NULL));
float* pfMem = (float*)pMem;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
pfMem[i] = rand();
}
//释放
VirtualFree(pMem, 4 * 4096, MEM_RELEASE);
//先保留再提交
LPBYTE pByte = (LPBYTE)VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE, PAGE_READWRITE);
VirtualAlloc(pByte + 4 * 4096, 4096, MEM_COMMIT, PAGE_READWRITE);
pfMem = (float*)(pByte + 4 * 4096);
for (int i = 0; i < 4096/sizeof(float); i++)
{
pfMem[i] = rand();
}
//释放
VirtualFree(pByte + 4 * 4096, 4096, MEM_DECOMMIT);
VirtualFree(pByte, 1024 * 1024, MEM_RELEASE);
- 大页面支持
//获得大页面的尺寸
DWORD dwLargePageSize = GetLargePageMinimum();
LPVOID pBuffer = VirtualAlloc(NULL, 64 * dwLargePageSize, MEM_RESERVE, PAGE_READWRITE);
//提交大页面
VirtualAlloc(pBuffer, 4 * dwLargePageSize, MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE);
VirtualFree(pBuffer, 4 * dwLargePageSize, MEM_DECOMMIT);
VirtualFree(pBuffer, 64 * dwLargePageSize, MEM_RELEASE);
VirtualProtect
VirtualProtect用来设置页面的保护属性,函数原型如下:
BOOL VirtualProtect(
LPVOID lpAddress, //虚拟内存地址
DWORD dwSize, //大小
DWORD flNewProtect, //保护属性
PDWORD lpflOldProtect //返回原来的保护属性
);
这个保护属性与之前介绍的VirtualAlloc中的保护属性相同,另外需要注意的一点是一般返回原来的属性的话,这个指针可以为NULL,但是这个函数不同,如果第四个参数为NULL,那么函数调用将会失败
LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4 * 4096 / sizeof(float); i++)
{
pfArray[i] = 1.0f * rand();
}
//将页面改为只读属性
DWORD dwOldProtect = 0;
VirtualProtect(pBuffer, 4 * 4096, PAGE_READONLY, &dwOldProtect);
//写入数据将发生异常
pfArray[9] = 0.1f;
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);
VirtualQuery
这个函数用来查询某段虚拟内存的属性信息,这个函数原型如下:
DWORD VirtualQuery(
LPCVOID lpAddress,//地址
PMEMORY_BASIC_INFORMATION lpBuffer, //用于接收返回信息的指针
DWORD dwLength //缓冲区大小,上述结构的大小
);
结构MEMORY_BASIC_INFORMATION的定义如下:
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress; //该页面的起始地址
PVOID AllocationBase;//分配给该页面的首地址
DWORD AllocationProtect;//页面的保护属性
DWORD RegionSize; //页面大小
DWORD State;//页面状态
DWORD Protect;//页面的保护类型
DWORD Type;//页面类型
} MEMORY_BASIC_INFORMATION;
typedef MEMORY_BASIC_INFORMATION *PMEMORY_BASIC_INFORMATION;
AllocationProtect与Protect所能取的值与之前的保护属性的值相同。 State的取值如下: MEM_FREE:空闲 MEM_RESERVE:保留 MEM_COMMIT:已提交 Type的取值如下: MEM_IMAGE:映射类型,一般是映射到地址控件的可执行模块如DLL,EXE等 MEM_MAPPED:文件映射类型 MEM_PRIVATE:私有类型,这个页面的数据为本进程私有数据,不能与其他进程共享 下面是这个的使用例子:
#include<windows.h>
#include <stdio.h>
#include <tchar.h>
#include <atlstr.h>
CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi);
int _tmain(int argc, TCHAR *argv[])
{
SYSTEM_INFO sm = {0};
GetSystemInfo(&sm);
LPVOID dwMinAddress = sm.lpMinimumApplicationAddress;
LPVOID dwMaxAddress = sm.lpMaximumApplicationAddress;
MEMORY_BASIC_INFORMATION mbi = {0};
_putts(_T("BaseAddress\tAllocationBase\tAllocationProtect\tRegionSize\tState\tProtect\tType\n"));
for (LPVOID pAddress = dwMinAddress; pAddress <= dwMaxAddress;)
{
if (VirtualQuery(pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == 0)
{
break;
}
_putts(GetMemoryInfo(&mbi));
//一般通过BaseAddress(页面基地址) + RegionSize(页面长度)来寻址到下一个页面的的位置
pAddress = (BYTE*)mbi.BaseAddress + mbi.RegionSize;
}
}
CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi)
{
CString lpMemoryInfo = _T("");
int iBaseAddress = (int)(pmi->BaseAddress);
int iAllocationBase = (int)(pmi->AllocationBase);
CString szProtected = _T("\0");
if (pmi->Protect & PAGE_READONLY)
{
szProtected = _T("R");
}else if (pmi->Protect & PAGE_READWRITE)
{
szProtected = _T("RW");
}else if (pmi->Protect & PAGE_WRITECOPY)
{
szProtected = _T("WC");
}else if (pmi->Protect & PAGE_EXECUTE)
{
szProtected = _T("X");
}else if (pmi->Protect & PAGE_EXECUTE_READ)
{
szProtected = _T("RX");
}else if (pmi->Protect & PAGE_EXECUTE_READWRITE)
{
szProtected = _T("RWX");
}else if (pmi->Protect & PAGE_EXECUTE_WRITECOPY)
{
szProtected = _T("WCX");
}else if (pmi->Protect & PAGE_GUARD)
{
szProtected = _T("GUARD");
}else if (pmi->Protect & PAGE_NOACCESS)
{
szProtected = _T("NOACCESS");
}else if (pmi->Protect & PAGE_NOCACHE)
{
szProtected = _T("NOCACHE");
}else
{
szProtected = _T(" ");
}
CString szAllocationProtect = _T("\0");
if (pmi->AllocationProtect & PAGE_READONLY)
{
szProtected = _T("R");
}else if (pmi->AllocationProtect & PAGE_READWRITE)
{
szProtected = _T("RW");
}else if (pmi->AllocationProtect & PAGE_WRITECOPY)
{
szProtected = _T("WC");
}else if (pmi->AllocationProtect & PAGE_EXECUTE)
{
szProtected = _T("X");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_READ)
{
szProtected = _T("RX");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_READWRITE)
{
szProtected = _T("RWX");
}else if (pmi->AllocationProtect & PAGE_EXECUTE_WRITECOPY)
{
szProtected = _T("WCX");
}else if (pmi->AllocationProtect & PAGE_GUARD)
{
szProtected = _T("GUARD");
}else if (pmi->AllocationProtect & PAGE_NOACCESS)
{
szProtected = _T("NOACCESS");
}else if (pmi->AllocationProtect & PAGE_NOCACHE)
{
szProtected = _T("NOCACHE");
}else
{
szProtected = _T(" ");
}
DWORD dwRegionSize = pmi->RegionSize;
CString strState = _T("");
if (pmi->State & MEM_FREE)
{
strState = _T("Free");
}else if (pmi->State & MEM_RESERVE)
{
strState = _T("Reserve");
}else if (pmi->State & MEM_COMMIT)
{
strState = _T("Commit");
}else
{
strState = _T(" ");
}
CString strType = _T("");
if (pmi->Type & MEM_IMAGE)
{
strType = _T("Image");
}else if (pmi->Type & MEM_MAPPED)
{
strType = _T("Mapped");
}else if (pmi->Type & MEM_PRIVATE)
{
strType = _T("Private");
}
lpMemoryInfo.Format(_T("%08X %08X %s %d %s %s %s\n"), iBaseAddress, iAllocationBase, szAllocationProtect, dwRegionSize, strState, szProtected, strType);
return lpMemoryInfo;
}
VirtualLock和VirtualUnlock
这两个函数用于锁定和解锁页面,前面说过操作系统会将长时间不用的内存中的数据放入到系统的磁盘文件中,需要的时候再放回到内存中,这样来回倒腾,必定会造成程序效率的底下,为了避免这中效率底下的操作,可以使用VirtualLock将页面锁定在内存中,防止页面交换,但是不用了的时候需要使用VirtualUnlock来解锁,不然一直锁定而不解锁会造成真实内存的不足。 另外需要注意的是,不能一次操作超过工作集规定的最大虚拟内存,这样会造成程序崩溃,我们可以通过函数SetProcessWorkingSetSize来设置工作集规定的最大虚拟内存的大小。下面是一个使用例子:
SetProcessWorkingSetSize(GetCurrentProcess(), 1024 * 1024, 2 * 1024 * 1024);
LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE, PAGE_READWRITE);
//不能锁定超过进程工作集大小的虚拟内存
VirtualLock(pBuffer, 3 * 1024 * 1024);
//不能一次提交超过进程工作集大小的虚拟内存
VirtualAlloc(pBuffer, 3 * 1024 * 1024, MEM_COMMIT, PAGE_READWRITE);
float *pfArray = (float*)pBuffer;
for (int i = 0; i < 4096 / sizeof(float); i++)
{
pfArray[i] = 1.0f * rand();
}
VirtualUnlock(pBuffer, 4096);
VirtualFree(pBuffer, 4096, MEM_DECOMMIT);
VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);
VirtualFree
VirtualFree用于释放申请的虚拟内存。这个函数支持反提交和释放,这两个操作由第三个参数指定: MEM_DECOMMIT:反提交,这样这个线性地址就不再映射到具体的物理内存,但是这个地址仍然是保留地址。 MEM_RELEASE:释放,这个范围的地址不再作为保留地址
南来地,北往的,上班的,下岗的,走过路过不要错过!
======================个性签名=====================
之前认为Apple 的iOS 设计的要比 Android 稳定,我错了吗?
下载的许多客户端程序/游戏程序,经常会Crash,是程序写的不好(内存泄漏?刚启动也会吗?)还是iOS本身的不稳定!!!
如果在Android手机中可以简单联接到ddms,就可以查看系统log,很容易看到程序为什么出错,在iPhone中如何得知呢?试试Organizer吧,分析一下Device logs,也许有用.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?