https://www.jianshu.com/p/aa64919f23ab
1. 内核驱动简介在实现Linux内核驱动中,开发者可以注册一个设备驱动文件,该文件常常在/dev/目录下完成注册。该文件可以支持所有的常规文件方法,比如opening,reading, writing, mmaping,closing等等。设备驱动文件支持的操作由包含了一组函数指针的结构体file_operations描述,每个指针描述一个操作。在4.9版本内核中可以找到如下的定义。
struct file_operations {
struct module *owner;
loff_t(*llseek) (struct file *, loff_t, int);
ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t(*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t(*write_iter) (struct kiocb *, struct iov_iter *);
int(*iterate) (struct file *, struct dir_context *);
int(*iterate_shared) (struct file *, struct dir_context *);
unsigned int(*poll) (struct file *, struct poll_table_struct *);
long(*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long(*compat_ioctl) (struct file *, unsigned int, unsigned long);
int(*mmap) (struct file *, struct vm_area_struct *);
int(*open) (struct inode *, struct file *);
int(*flush) (struct file *, fl_owner_t id);
int(*release) (struct inode *, struct file *);
int(*fsync) (struct file *, loff_t, loff_t, int datasync);
int(*fasync) (int, struct file *, int);
int(*lock) (struct file *, int, struct file_lock *);
ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long(*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int(*check_flags)(int); int(*flock) (struct file *, int, struct file_lock *);
ssize_t(*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t(*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int(*setlease)(struct file *, long, struct file_lock **, void **);
long(*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
void(*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned(*mmap_capabilities)(struct file *);
#endif
ssize_t(*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
int(*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64);
ssize_t(*dedupe_file_range)(struct file *, u64, u64, struct file *, u64);
};
如同上面展示,可以实现非常多的文件操作,本文的主角是mmap handler的实现。
file_operations结构体的安装示例以及相关联的函数可以在下面看到
('/fs/proc/softirqs.c'):
static int show_softirqs(struct seq_file *p, void *v)
{
int i, j;
seq_puts(p, " ");
for_each_possible_cpu(i)
seq_printf(p, "CPU%-8d", i);
seq_putc(p, '\n');
for (i = 0; i < NR_SOFTIRQS; i++)
{
seq_printf(p, "%12s:", softirq_to_name[i]);
for_each_possible_cpu(j)
seq_printf(p, " %10u", kstat_softirqs_cpu(i, j));
seq_putc(p, '\n');
}
return 0;
}
static int softirqs_open(struct inode *inode, struct file *file)
{
return single_open(file, show_softirqs, NULL);
}
static const struct file_operations proc_softirqs_operations = {
.open = softirqs_open,
.read = seq_read,
.llseek = seq_lseek,
.release = single_release,
};
static int __init proc_softirqs_init(void)
{
proc_create("softirqs", 0, NULL, &proc_softirqs_operations);
return 0;
}
上述代码可以在'proc_softirqs_operations'结构体中看到,它允许调用open,read,llseek和close函数。当一个应用程序试图去打开一个'softirqs'文件时就会调用'open'系统调用,进而会调用到指向的'softirqs_open'函数。2. 内核mmap Handler2.1 简单的mmap Handler如上文提及,内核驱动可以实现自己的mmap handler。主要目的在于mmap handler可以加速用户空间进程和内核空间的数据交换。内核可以共享一块内核buffer或者直接共享某些物理内存地址范围给用户空间。用户空间进程可以直接修改这块内存而无需调用额外的系统调用。一个简单(并且不安全)的mmap handler实现例子如下:
static struct file_operations fops =
{
.open = dev_open,
.mmap = simple_mmap,
.release = dev_release,
};
int size = 0x10000;
static int dev_open(struct inode *inodep, struct file *filep)
{
printk(KERN_INFO "MWR: Device has been opened\n");
filep->private_data = kzalloc(size, GFP_KERNEL);
if (filep->private_data == NULL)
return -1;
return 0;
}
static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
if (
remap_pfn_range( vma, vma->vm_start,
virt_to_pfn(filp->private_data), vma->vm_end - vma->vm_start,
vma->vm_page_prot ) )
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}
当打开上面的驱动时,dev_open会被调用,它简单的分配0x10000字节的buffer并且将其保存在private_data指针域。此后如果进程在对该文件描述符调用mmap时,就会调用到simple_mmap。该函数简单的调用 remap_pfn_range 函数来创建一个进程地址空间的新映射,将private_data指向的buffer和vma->vm_start开始的尺寸为vma->vm_end-vma->vm_start 大小的地址空间关联起来。一个请求对应文件mmap的用户空间程序样例:
int main(int argc, char * const * argv)
{
int fd = open("/dev/MWR_DEVICE", O_RDWR);
if (fd < 0)
{
printf("[-] Open failed!\n");
return -1;
}
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);
if (addr == MAP_FAILED)
{
perror("Failed to mmap: ");
close(fd);
return -1;
}
printf("mmap OK addr: %lx\n", addr);
close(fd);
return 0;
}
上面的代码对/dev/MWR_DEVICE驱动文件调用了mmap,大小为0x1000,文件偏移设置为0x1000,目标地址设置为0x42424000。可以看到一个成功的映射结果:
# cat /proc/23058/maps
42424000-42425000 rw-s 00001000 00:06 68639 /dev/MWR_DEVICE
2.2 空的mmap Handler到目前为止,我们已经见过了最简单的mmap操作的实现体,但是如果mmap handler是个空函数的话,会发生什么?让我们考虑这个实现:
static struct file_operations fops =
{
.open = dev_open,
.mmap = empty_mmap,
.release = dev_release,
};
static int empty_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: empty_mmap\n");
return 0;
}
如我们所见,函数中只有log信息,这是为了让我们观察到函数被调用了。当empty_mmap被调用时,我们毫不夸张的可以猜测到什么都不会发生,mmap会引发失败,毕竟此时并没有remap_pfn_range或其他类似的函数。然而,事实并非如此。让我们运行一下用户空间代码,看看究竟会发生什么:
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x1000;
unsigned long *addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);
在dmesg log中我们可以看到空的handler被成功调用:
[ 1119.393560] MWR: Device has been opened 1 time(s)
[ 1119.393574] MWR: empty_mmap
看看内存映射,有没有什么异常:
# cat /proc/2386/maps
42424000-42426000 rw-s 00001000 00:06 22305
我们并没有调用remap_pfn_range函数,然而映射却如同此前情景那样被创建了。唯一的不同在于映射是无效的,因为我们实际上并没有映射任何的物理内存给这块虚拟地址。这样的一个mmap实现中,一旦访问了映射的地址空间,要么引起进程崩溃,要么引起整个内核的崩溃,这取决于具体使用的内核。让我们试试访问这块内存:
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x1000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000,
size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000); printf("addr[0]:
%x\n", addr[0]);
如我们所愿,进程崩溃了:
./mwr_client
Bus error
然而在某些3.10 arm/arm64 Android内核中,类似的代码会引起kernel panic。所以说,作为一个开发者,你不应该假定一个空的handler可以按预期表现,在内核中始终使用一个可用的返回码来控制给定的情形。一个带有vm_operations_struct的mmap Handler在mmap操作中,有办法在已分配内存区间上使用vm_operations_struct结构体来指派多种其他操作的handler(例如控制unmapped memory, page permission changes等)。vm_operations_struct在kernel 4.9中的定义如下:
struct vm_operations_struct {
void(*open)(struct vm_area_struct * area);
void(*close)(struct vm_area_struct * area);
int(*mremap)(struct vm_area_struct * area);
int(*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
int(*pmd_fault)(struct vm_area_struct *, unsigned long address, pmd_t *, unsigned int flags);
void(*map_pages)(struct fault_env *fe, pgoff_t start_pgoff, pgoff_t end_pgoff);
int(*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
int(*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);
int(*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int len, int write);
const char *(*name)(struct vm_area_struct *vma);
#ifdef CONFIG_NUMA
int(*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);
struct mempolicy *(*get_policy)(struct vm_area_struct *vma, unsigned long addr);
#endif
struct page *(*find_special_page)(struct vm_area_struct *vma, unsigned long addr);
};
如上文描述,这些函数指针可以用于实现特定的handler。关于此的详细描述在《Linux Device Drivers(Linux设备驱动)》一书中可以找到。在实现内存分配器时,一个通俗可见的主流的行为是开发者实现了一个'fault' handler。例如,看看这一段:
static struct file_operations fops = {
.open = dev_open,
.mmap = simple_vma_ops_mmap,
.release = dev_release,
};
static struct vm_operations_struct simple_remap_vm_ops = {
.open = simple_vma_open,
.close = simple_vma_close,
.fault = simple_vma_fault,
};
static int simple_vma_ops_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device simple_vma_ops_mmap\n");
vma->vm_private_data = filp->private_data;
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}
void simple_vma_open(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "MWR: Simple VMA open, virt %lx, phys %lx\n", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);
}
void simple_vma_close(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "MWR: Simple VMA close.\n");
}
int simple_vma_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
struct page *page = NULL;
unsigned long offset;
printk(KERN_NOTICE "MWR: simple_vma_fault\n");
offset = (((unsigned long)vmf->virtual_address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT));
if (offset > PAGE_SIZE << 4)
goto nopage_out;
page = virt_to_page(vma->vm_private_data + offset);
vmf->page = page;
get_page(page);
nopage_out:
return 0;
}
上述代码中我们可以看到simple_vma_ops_mmap函数用于控制mmap调用。它什么都没做,除了指派了simple_remap_vm_ops结构体作为虚拟内存操作的handler。让我们看看下列代码在该driver上运行的效果:
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x1000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);
dmesg的结果:
[268819.067085] MWR: Device has been opened 2 time(s)
[268819.067121] MWR: Device simple_vma_ops_mmap
[268819.067123] MWR: Simple VMA open, virt 42424000, phys 1000
[268819.067125] MWR: Device mmap OK
进程地址空间的映射:42424000-42425000 rw-s 00001000 00:06 140215 /dev/MWR_DEVICE如我们所见,simple_vma_ops_mmap函数被调用了,内存映射也创建了。例子中simple_vma_fault函数没有被调用。问题在于,我们有了个地址范围为0x42424000-0x42425000的地址空间却不清楚它指向何处。我们没有为它关联物理内存,因此当进程试图访问这段地址的任一部分时,simple_vma_fault都会执行。所以让我们看看这段用户空间代码:
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x2000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);
printf("addr[0]: %x\n", addr[0]);
代码唯一的改变在于使用了printf函数来访问映射内存。因为内存区域无效所以我们的simple_vma_fault例程被调用了。dmesg的输出可以看到:
[285305.468520] MWR: Device has been opened 3 time(s)
[285305.468537] MWR: Device simple_vma_ops_mmap
[285305.468538] MWR: Simple VMA open, virt 42424000, phys 1000
[285305.468539] MWR: Device mmap OK
[285305.468546] MWR: simple_vma_fault
在simple_vma_fault函数中,我们可以观察到offset变量使用了指向一个没有被映射的地址的vmf->virtual_address进行了计算。我们这里就是addr[0]的地址。下一个page结构体由virt_to_page宏得到,该宏将新获取的page赋值给vmf->page变量。这一赋值意味着当fault handler返回时,addr[0]会指向由simple_vma_fault计算出来的某个物理地址。该内存可以被用户进程所访问而无需其他任何代码。如果程序试图访问addr[513](假定sizeof(unsigned long)为8),fault handler会被再次调用,这是由于addr[0]和addr[513]在两个不同的内存页上,而此前仅有一个内存页被映射过。这就是源码:
int fd = open("/dev/MWR_DEVICE", O_RDWR);
unsigned long size = 0x2000;
unsigned long * addr = (unsigned long *)mmap((void*)0x42424000, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0x1000);
printf("addr[0]: %x\n", addr[0]);
printf("addr[513]: %x\n", addr[513]);
生成内核log:
[286873.855849] MWR: Device has been opened 4 time(s)
[286873.855976] MWR: Device simple_vma_ops_mmap
[286873.855979] MWR: Simple VMA open, virt 42424000, phys 1000
[286873.855980] MWR: Device mmap OK
[286873.856046] MWR: simple_vma_fault
[286873.856110] MWR: simple_vma_fault
3. 经典mmap Handler议题3.1 用户输入有效性的不足让我们看看前面的mmap handler例子:
static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
if (
remap_pfn_range( vma, vma->vm_start,
virt_to_pfn(filp->private_data), vma->vm_end - vma->vm_start,
vma->vm_page_prot ) )
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}
前面展示的代码展示了一个通用的实现mmap handler的途径,相似的代码可以在《Linux设备驱动》一书中找到。示例代码主要的议论点在于vma->vm_end和vma->vm_start的值从未检查有效性。取而代之的,它们被直接传递给remap_pfn_range作为尺寸参数。这意味着一个恶意进程可以用一个不受限的尺寸来调用mmap。在我们这里,允许一个用户空间进程去mmap所有的在filp->private_databuffer之后的物理内存地址空间。这包括所有的内核内存。这意味着恶意进程能够从用户空间读写整个内核内存。另一个流行的用法如下:
static int simple_mmap(struct file *filp, struct vm_area_struct *vma)
{
printk(KERN_INFO "MWR: Device mmap\n");
if ( remap_pfn_range( vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot ) )
{
printk(KERN_INFO "MWR: Device mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device mmap OK\n");
return 0;
}
上面的代码中我们可以看到用户控制的offset vma->vm_pgoff被直接传递给了remap_pfn_range函数作为物理地址。这会使得恶意进程有能力传递一个任意物理地址给mmap,也就在用户空间拥有了整个内核内存的访问权限。在一些对示例进行微小改动的情景中经常可以看到,要么offset有了掩码,要么使用了另外一个值来计算。3.2 整数溢出经常可以看到开发者试图使用复杂的计算、按位掩码、位移、尺寸和偏移和等方法去验证映射的尺寸和偏移(size and offset)。不幸的是,这常常导致了创建的复杂性以及不寻常的计算和验证过程晦涩难懂。在对size和offset值进行少量fuzzing后,找到可以绕过有效性检查的值并非不可能。让我们看看这段代码:
static int integer_overflow_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned int vma_size = vma->vm_end - vma->vm_start;
unsigned int offset = vma->vm_pgoff << PAGE_SHIFT;
printk(KERN_INFO "MWR: Device integer_overflow_mmap( vma_size: %x, offset: %x)\n", vma_size, offset);
if (vma_size + offset > 0x10000)
{
printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of memory\n");
return -EAGAIN;
}
if (remap_pfn_range(vma, vma->vm_start, virt_to_pfn(filp->private_data), vma_size, vma->vm_page_prot))
{
printk(KERN_INFO "MWR: Device integer_overflow_mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device integer_overflow_mmap OK\n");
return 0;
}
上面的代码展示了一个典型的整数溢出漏洞,它发生在一个进程调用mmap2系统调用时使用了size为0xfffa000以及offset为0xf0006的情况。0xfffa000和0xf0006000的和等于0x100000000。由于最大的unsigned int值为0xffffffff,最高符号位会被清掉,最终的和会变成0x0。这种情况下,mmap系统调用会成功绕过检查,进程会访问到预期buffer外的内存。如上文提到的,有两个独立的系统调用mmap和mmap2。mmap2使得应用程序可以使用一个32位的off_t类型来映射大文件(最大为2^44字节),这是通过支持使用一个大数offset参数实现的。有趣的是mmap2系统调用通常在64位内核系统调用表中不可用。然而,如果操作系统同时支持32位和64位进程,他就通常在32位进程中可用。这是因为32位和64位进程各使用独立的系统调用表。3.3 有符号整型类型另一个老生常谈的议题就是size变量的有符号类型。让我们看看这段代码:
static int signed_integer_mmap(
struct file *filp, struct vm_area_struct *vma)
{
int vma_size = vma->vm_end - vma->vm_start;
int offset = vma->vm_pgoff << PAGE_SHIFT;
printk(KERN_INFO "MWR: Device signed_integer_mmap( vma_size: %x, offset: %x)\n", vma_size, offset);
if (vma_size > 0x10000 || offset < 0 || offset > 0x1000 || (vma_size + offset > 0x10000))
{
printk(KERN_INFO "MWR: mmap failed, requested too large a chunk of memory\n");
return -EAGAIN;
}
if (remap_pfn_range(vma, vma->vm_start, offset, vma->vm_end - vma->vm_start, vma->vm_page_prot))
{
printk(KERN_INFO "MWR: Device signed_integer_mmap failed\n");
return -EAGAIN;
}
printk(KERN_INFO "MWR: Device signed_integer_mmap OK\n");
return 0;
}
上述代码中,用户控制数据存储在vma_size和offset变量中,它们都是有符号整型。size和offset检查是这一行:if (vma_size > 0x10000 || offset < 0 || offset > 0x1000 || (vma_size + offset > 0x10000))不幸的是,因为vma_size被声明为有符号整型数,一种攻击手法是通过使用负数诸如0xf0000000来绕过这个检查。这回引起0xf0000000字节被映射到用户空间地址。本文由看雪论坛玉涵 编译,来源exploit-database-papers转载请注明来自看雪社区
作者:看雪学院
链接:https://www.jianshu.com/p/aa64919f23ab
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
- 程序优化
- vtune
- linux性能监控软件Perf
- 系统级性能分析工具perf的介绍与使用
- perf的二级命令
- 全局性概况
- 全局细节
- 最常用功能perf record
- 可视化工具perf timechart
- perf引入的overhead
- perf stat
- gprof
- 三种Linux性能分析工具的比较
- perf+gprof+gprof2dot+graphviz进行性能分析热点
- 英特尔多核平台编程优化大赛报告
- 内存操作
- mmap
- mmap的分类
- 深入理解内存映射mmap
- 计算机底层知识拾遗(九)深入理解内存映射mmap
- 内核驱动mmap Handler利用技术(一)
- Windows内存管理机制及C++内存分配实例
- Linux内存管理初探
- Windows CPU信息查看
- Linux CPU信息查看
- 预留大内存
- Linux下试验大页面映射
- /dev/mem
- Linux中通过/dev/mem操控物理地址
- /dev/mem分析
- 用法举例
- Linux下直接读写物理地址内存
- 查看内存信息
- Cache Memory
- 页面缓存
- 查看各级cache信息的方法
- dmidecode命令查看cache size
- CPU Cache 机制以及 Cache miss
- ARM体系关闭mmu和cache
- CR0-4寄存器介绍
- 查看CR0,CR2,CR3的值
- Linux 下如何禁用CPU cache
- 7个示例科普CPU Cache
- 第一个例子的C代码
- 其中之一
- Linux 从虚拟地址到物理地址
- 内存测试例子
- 每个程序员都应该了解的内存
- Part 1
- 程序员能够做什么
- 3 CPU caches
- 6 What Programmers Can Do
- VirtualAlloc
- Large-Page Support
- Some remarks on VirtualAlloc and MEM_LARGE_PAGES
- DMA
- MOV和MOVS的效率问题?如何高效的拷贝内存 中的数据
- how to use movntdqa to avoid cache pollution
- 计算机底层知识拾遗(一)理解虚拟内存机制
- How to access the control registers cr0,cr2,cr3 from a program
- 细说Cache-L1/L2/L3/TLB
- what-is-the-meaning-of-non-temporal-memory-accesses-in-x86
- How can the L1, L2, L3 CPU caches be turned off on modern x86/amd64 chips?
- UA list
- GDB
- 程序运行参数
- Linux下GDB的多线程调试
- CMake
- CMake快速入门教程:实战
- cmake打印变量值
- function
- source_group
- cmake_parse_arguments
- 编译.S文件
- add_definitions
- CMake添加-g编译选项
- Debug模式下启动
- Mysql
- Mysql联合查询union和union all的使用介绍
- MySQL数据库导入错误:ERROR 1064 (42000) 和 ERROR at line xx: Unknown command '\Z'.
- 解决MYSQL数据库 Table ‘xxx’ is marked as crashed and should be repaired 145错误
- C/C++
- c语言中static的作用
- strlen和sizeof有什么区别?
- printf
- Libuv中文文档之线程
- RapidJSON
- gcc/g++ 实战之编译的四个过程
- __thread
- TARGET_LINK_LIBRARIES
- MAP_HUGETLB
- 使用Intel格式的汇编
- __m128i
- emmintrin.h
- _mm_stream_si128
- _mm_stream_load_si128
- _mm_load_si128
- _mm_xor_si128
- _mm_store_si128
- _mm_cvtsi128_si64
- Intel SSE指令集
- _mm_set_epi64x
- _mm_aesenc_si128
- _umul128
- _mm_malloc
- reinterpret_cast
- strlen
- 读取UTF-8的txt文件发现开头的多三个字节的问题
- PHP
- php计算函数执行时间的方法
- 框架
- Json Rpc远程调用框架
- PHP多进程
- PHP CLI模式下的多进程应用
- php多进程总结
- 优化
- PHP7 优化
- 让你的PHP7更快(GCC PGO)
- PHP的性能演进(从PHP5.0到PHP7.1的性能全评测)
- PHP字符串全排列算法
- 获取服务器基本信息
- cookie
- phpstudy2018 安装xdebug扩展
- 软件下载
- PHP mysqli_error() 函数
- PHP Session 变量
- curl
- curl_getinfo
- 获取请求头
- PHP使用CURL获取302跳转后的地址实例
- PHP基于cURL实现自动模拟登录
- PHP获取远程图片大小(CURL实现)
- CURL模拟登录
- curl模拟登录提交(从目录中获取文件)
- CURL HTTPS
- curl帮v
- rename
- copy
- JSON
- json_encode
- json_decode
- json_last_error_msg
- json_last_error
- PHP json_encode中文乱码解决方法
- var_dump
- PHPStorm与Xdebug设置
- Xdebug原理以notepad为例
- str_pad
- pack
- PHP二进制与字符串之间的相互转换
- PHP执行系统命令(简介及方法)
- 函数
- 十进制转二进制
- 字符串到ASSCI
- 字符串转二进制
- 合并两个表
- 图像识别
- Tesseract
- 虚拟机
- vmware下Kali 2.0安装VMware Tools
- 安装 VMware tools出现“正在进行简易安装时,无法手动启动VMware tools安装”
- 爬虫
- 有哪些好的数据来源或者大数据平台?
- Cygwin
- Git 常用命令
- 排列组合
- 含重复元素序列的全排列
- 全排列的非递归和递归实现(含重复元素)
- GitBook
- 编辑环境
- visual studio code
- 2名数学家或发现史上最快超大乘法运算法,欲破解困扰人类近半个世纪的问题
- 系统预定义常量
- 指令集
- SSE
- _MSC_VER
- msys2
- 安装cmake
- MSYS2 更新源
- 讲Cmake msys32使用问题解答 CXX CMAKE_C_COMPILER配置详解
- VirtualBox
- 解决virtualbox只能安装32位系统的问题
- Ubuntu
- 使用AES-NI的编译参数
- debian下安装内核源码的方法
- tar.xz结尾的文件的解压方法
- Linux命令
- insmod
- fatal error: openssl/bio.h
- 准备module的编译环境(kali)
- Ubuntu/Debian 之内核模块开发准备
- dmesg的详细用法
- Linux系统开机自动加载驱动module
- linux /Module 浅析(转载)
- Kali
- 找回gpedit
- Enable the Lock Pages in Memory Option (Windows)
- TLA
- 双系统
- 显卡
- 显示no CUDA的解决过程
