[转帖]虚拟内存探究 -- 第一篇:C strings & /proc
虚拟内存探究 -- 第一篇:C strings & /proc
这是虚拟内存系列文章的第一篇。
本文通过实验的手段, 带大家了解一些计算机科学相关的基础知识。
在本文,我们将利用/proc查找进程虚拟内存中的ASCII字符串, 然后修改该字符串。
在这一过程中,我们将学到很多有趣的东西。
一、实验环境
所有的脚本和程序都在下面的环境中测试过:
- Ubuntu 14.04 LTS
- Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
- gcc
- gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
- Python 3
- Python 3.4.3 (default, Nov 17 2016, 01:08:31)
- [GCC 4.8.4] on linux
二、预备知识
阅读本文所需知识:
- C语言基础
- 些许Python知识
- 了解Linux的文件系统和shell命令
三、虚拟内存
在计算机领域, 虚拟内存是通过软硬件结合实现的一种内存管理技术, 它将程序所使用的内存地址(虚拟内存地址)映射到计算机的物理内存上(物理内存地址),这使得每个程序看到的内存地址空间都是连续的(或是一些连续地址空间的集合)。操作系统管理虚拟地址空间, 以及虚拟地址空间到物理内存的映射。CPU中的地址转换硬件(通常被称为内存管理单元, MMU)自动将虚拟内存地址转换成物理内存地址。操作系统可以提供比实际物理内存更多的虚拟内存,这一行为是通过操作系统中的软件来实现的。
虚拟内存的主要好处包含以下几点:
- 将应用程序从内存管理中解放出来, 应用程序只需关心自己的逻辑
- 不同应用程序间的虚拟内存是相互隔离的, 所以安全性增加了
- 结合内存分页管理技术, 应用程序理论上可使用比物理内存更多的内存空间
有关虚拟内存的知识, 可进一步阅读维基百科上的相关介绍:虚拟内存。
在虚拟内存探究 – 第三篇:一步一步画虚拟内存图中,我们将探索虚拟内存的更多细节,并且看下虚拟内存中都有些什么, 以及这些东西分别位于虚拟内存的什么地方。
继续阅读本文前, 你需要知道以下几点:
- 每个进程都有自己独立的虚拟内存
- 虚拟内存大小依赖于计算机系统架构
- 不同的操作系统对虚拟内存的处理会有所不同, 对于现代的大多数操作系统来说, 虚拟内存如下所示:

在虚拟内存的高地址空间,我们可以看到(下面仅列出了部分内容,并非全部):
- 命令行参数和环境变量
- “向下”生长的栈。咋看之下这是违反直觉的,但这确实是虚拟内存中栈的实现方式。
在虚拟内存的低地址空间, 我们可以看到:
- 可执行程序(实际上远比这复杂,但对于理解本文剩余内容足够了)
- “向上”生长的堆
堆是虚拟内存的一部分,动态分配的内存(比如用malloc分配的内存)位于堆中。
请时刻记住, 虚拟内存和物理内存是不同的。
四、剖析一个简单的C程序
我们从一个简单的C程序开始:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/**
* main - uses strdup to create a new string, and prints the
* address of the new duplcated string
*
* Return: EXIT_FAILURE if malloc failed. Otherwise EXIT_SUCCESS
*/
int main(void)
{
char *s;
s = strdup("Holberton");
if (s == NULL)
{
fprintf(stderr, "Can't allocate mem with malloc\n");
return (EXIT_FAILURE);
}
printf("%p\n", (void *)s);
return (EXIT_SUCCESS);
}
strdup
继续阅读本文前,请想一想strdup是如何复制字符串Holberton的?又该如何证明呢?
.
.
.
strdup必须创建一个新的字符串,所以它首先会为新字符串手分配内存, 并且可能使用malloc分配内存。这可以通过strdup的man手册确认:
DESCRIPTION
The strdup() function returns a pointer to a new string which is a duplicate of the string s.
Memory for the new string is obtained with malloc(3), and can be freed with free(3).
等等,基于上文关于虚拟内存的知识,你认为新创建的字符串位于虚拟内存的哪里呢?是高地址空间还是低地址空间?
.
.
.
应该是位于低地址空间(堆中)。为了验证我们的推断,让我们编译并执行这个C程序:
julien@holberton:~/holberton/w/hackthevm0$ gcc -Wall -Wextra -pedantic -Werror main.c -o holberton
julien@holberton:~/holberton/w/hackthevm0$ ./holberton
0x1822010
julien@holberton:~/holberton/w/hackthevm0$
新创建的字符串地址是0x1822010。但它究竟是高地址还是低地址呢?
进程的虚拟内存空间多大
进程虚拟地址空间的大小依赖于计算机系统架构。我运行本例使用的是64位机器,所以理论上每个进程的虚拟内存是2^64字节,内存最高地址是0xffffffffffffffff (1.8446744e+19),最低地址是0x0。
0x1822010小于0xffffffffffffffff,所以新字符串可能是位于低地址。借助proc文件系统,我们可以确认这一点。
proc文件系统
由man proc可见:
The proc filesystem is a pseudo-filesystem which provides an interface to kernel data structures. It is commonly mounted at `/proc`. Most of it is read-only, but some files allow kernel variables to be changed.
/proc目录包含一系列文件,我们只关注其中的两个:
/proc/[pid]/mem/proc/[pid]/maps
mem文件
由man proc可见:
/proc/[pid]/mem
This file can be used to access the pages of a process's memory
through open(2), read(2), and lseek(2).
太棒了!如此看来, 我们可以访问并修改任一进程的虚拟内存。
maps文件
由man proc可见:
/proc/[pid]/maps
A file containing the currently mapped memory regions and their access permissions.
See mmap(2) for some further information about memory mappings.
The format of the file is:
address perms offset dev inode pathname
00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/dbus-daemon
00651000-00652000 r--p 00051000 08:02 173521 /usr/bin/dbus-daemon
00652000-00655000 rw-p 00052000 08:02 173521 /usr/bin/dbus-daemon
00e03000-00e24000 rw-p 00000000 00:00 0 [heap]
00e24000-011f7000 rw-p 00000000 00:00 0 [heap]
...
35b1800000-35b1820000 r-xp 00000000 08:02 135522 /usr/lib64/ld-2.15.so
35b1a1f000-35b1a20000 r--p 0001f000 08:02 135522 /usr/lib64/ld-2.15.so
35b1a20000-35b1a21000 rw-p 00020000 08:02 135522 /usr/lib64/ld-2.15.so
35b1a21000-35b1a22000 rw-p 00000000 00:00 0
35b1c00000-35b1dac000 r-xp 00000000 08:02 135870 /usr/lib64/libc-2.15.so
35b1dac000-35b1fac000 ---p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so
35b1fac000-35b1fb0000 r--p 001ac000 08:02 135870 /usr/lib64/libc-2.15.so
35b1fb0000-35b1fb2000 rw-p 001b0000 08:02 135870 /usr/lib64/libc-2.15.so
...
f2c6ff8c000-7f2c7078c000 rw-p 00000000 00:00 0 [stack:986]
...
7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0 [stack]
7fffb2d48000-7fffb2d49000 r-xp 00000000 00:00 0 [vdso]
The address field is the address space in the process that the mapping occupies.
The perms field is a set of permissions:
r = read
w = write
x = execute
s = shared
p = private (copy on write)
The offset field is the offset into the file/whatever;
dev is the device (major:minor); inode is the inode on that device. 0 indicates
that no inode is associated with the memory region,
as would be the case with BSS (uninitialized data).
The pathname field will usually be the file that is backing the mapping.
For ELF files, you can easily coordinate with the offset field
by looking at the Offset field in the ELF program headers (readelf -l).
There are additional helpful pseudo-paths:
[stack]
The initial process's (also known as the main thread's) stack.
[stack:<tid>] (since Linux 3.4)
A thread's stack (where the <tid> is a thread ID).
It corresponds to the /proc/[pid]/task/[tid]/ path.
[vdso] The virtual dynamically linked shared object.
[heap] The process's heap.
If the pathname field is blank, this is an anonymous mapping as obtained via the mmap(2) function.
There is no easy way to coordinate
this back to a process's source, short of running it through gdb(1), strace(1), or similar.
Under Linux 2.0 there is no field giving pathname.
这意味着我们可以通过文件/proc/[pid]/mem找到一个正在运行的进程的堆。如果可以读取堆内容,我们就能找到想要修改的字符串。如果能够往堆中写数据,我们就可以进行字符串替换。
pid
进程是可执行程序的一个实例,拥有一个全局唯一的进程ID(PID)。有些函数/系统调用使用PID操作进程或者和进程进行交互。
可以通过命令ps来查看某个进程的pid(具体请参阅man ps)。
五、替换进程的字符串
我们接下来要在一个进程的堆中搜索特定字符串,并用另一个字符串(长度不大于原字符串)替换它。 现在我们已经掌握了所需要的理论知识。
下面这个程序是我们将要hack的程序,正常情况下它循环输出字符串Holberton。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
/**
* main - uses strdup to create a new string, loops forever-ever
*
* Return: EXIT_FAILURE if malloc failed. Other never returns
*/
int main(void)
{
char *s;
unsigned long int i;
s = strdup("Holberton");
if (s == NULL)
{
fprintf(stderr, "Can't allocate mem with malloc\n");
return (EXIT_FAILURE);
}
i = 0;
while (s)
{
printf("[%lu] %s (%p)\n", i, s, (void *)s);
sleep(1);
i++;
}
return (EXIT_SUCCESS);
}
编译运行该程序,它将循环输出字符串Holberton直到进程被杀死。
julien@holberton:~/holberton/w/hackthevm0$ gcc -Wall -Wextra -pedantic -Werror loop.c -o loop
julien@holberton:~/holberton/w/hackthevm0$ ./loop
[0] Holberton (0xfbd010)
[1] Holberton (0xfbd010)
[2] Holberton (0xfbd010)
[3] Holberton (0xfbd010)
[4] Holberton (0xfbd010)
[5] Holberton (0xfbd010)
[6] Holberton (0xfbd010)
[7] Holberton (0xfbd010)
...
感兴趣的话,你可以暂停阅读本文,尝试写个脚本/程序寻找进程堆中的字符串。
.
.
.
/proc探究
首先运行刚才编译出来的可执行程序loop.
julien@holberton:~/holberton/w/hackthevm0$ ./loop
[0] Holberton (0x10ff010)
[1] Holberton (0x10ff010)
[2] Holberton (0x10ff010)
[3] Holberton (0x10ff010)
...
然后找到这个进程的ID
julien@holberton:~/holberton/w/hackthevm0$ ps aux | grep ./loop | grep -v grep
julien 4618 0.0 0.0 4332 732 pts/14 S+ 17:06 0:00 ./loop
上面的例子中, 进程ID是4618(不同机器上不同时刻运行例程,进程ID都可能不同)。所以,我们将要查看的文件maps和mem位于目录/proc/4618:
/proc/4618/maps/proc/4618/mem
在目录/proc/4618下执行ls -la可以看到以下内容:
julien@ubuntu:/proc/4618$ ls -la
total 0
dr-xr-xr-x 9 julien julien 0 Mar 15 17:07 .
dr-xr-xr-x 257 root root 0 Mar 15 10:20 ..
dr-xr-xr-x 2 julien julien 0 Mar 15 17:11 attr
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 autogroup
-r-------- 1 julien julien 0 Mar 15 17:11 auxv
-r--r--r-- 1 julien julien 0 Mar 15 17:11 cgroup
--w------- 1 julien julien 0 Mar 15 17:11 clear_refs
-r--r--r-- 1 julien julien 0 Mar 15 17:07 cmdline
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 comm
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 coredump_filter
-r--r--r-- 1 julien julien 0 Mar 15 17:11 cpuset
lrwxrwxrwx 1 julien julien 0 Mar 15 17:11 cwd -> /home/julien/holberton/w/funwthevm
-r-------- 1 julien julien 0 Mar 15 17:11 environ
lrwxrwxrwx 1 julien julien 0 Mar 15 17:11 exe -> /home/julien/holberton/w/funwthevm/loop
dr-x------ 2 julien julien 0 Mar 15 17:07 fd
dr-x------ 2 julien julien 0 Mar 15 17:11 fdinfo
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 gid_map
-r-------- 1 julien julien 0 Mar 15 17:11 io
-r--r--r-- 1 julien julien 0 Mar 15 17:11 limits
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 loginuid
dr-x------ 2 julien julien 0 Mar 15 17:11 map_files
-r--r--r-- 1 julien julien 0 Mar 15 17:11 maps
-rw------- 1 julien julien 0 Mar 15 17:11 mem
-r--r--r-- 1 julien julien 0 Mar 15 17:11 mountinfo
-r--r--r-- 1 julien julien 0 Mar 15 17:11 mounts
-r-------- 1 julien julien 0 Mar 15 17:11 mountstats
dr-xr-xr-x 5 julien julien 0 Mar 15 17:11 net
dr-x--x--x 2 julien julien 0 Mar 15 17:11 ns
-r--r--r-- 1 julien julien 0 Mar 15 17:11 numa_maps
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 oom_adj
-r--r--r-- 1 julien julien 0 Mar 15 17:11 oom_score
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 oom_score_adj
-r-------- 1 julien julien 0 Mar 15 17:11 pagemap
-r-------- 1 julien julien 0 Mar 15 17:11 personality
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 projid_map
lrwxrwxrwx 1 julien julien 0 Mar 15 17:11 root -> /
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 sched
-r--r--r-- 1 julien julien 0 Mar 15 17:11 schedstat
-r--r--r-- 1 julien julien 0 Mar 15 17:11 sessionid
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 setgroups
-r--r--r-- 1 julien julien 0 Mar 15 17:11 smaps
-r-------- 1 julien julien 0 Mar 15 17:11 stack
-r--r--r-- 1 julien julien 0 Mar 15 17:07 stat
-r--r--r-- 1 julien julien 0 Mar 15 17:11 statm
-r--r--r-- 1 julien julien 0 Mar 15 17:07 status
-r-------- 1 julien julien 0 Mar 15 17:11 syscall
dr-xr-xr-x 3 julien julien 0 Mar 15 17:11 task
-r--r--r-- 1 julien julien 0 Mar 15 17:11 timers
-rw-r--r-- 1 julien julien 0 Mar 15 17:11 uid_map
-r--r--r-- 1 julien julien 0 Mar 15 17:11 wchan
/proc/pid/maps
如之前所见,文件/proc/pid/maps是个文本文件,我们可以直接读取,内容如下:
julien@ubuntu:/proc/4618$ cat maps
00400000-00401000 r-xp 00000000 08:01 1070052 /home/julien/holberton/w/funwthevm/loop
00600000-00601000 r--p 00000000 08:01 1070052 /home/julien/holberton/w/funwthevm/loop
00601000-00602000 rw-p 00001000 08:01 1070052 /home/julien/holberton/w/funwthevm/loop
010ff000-01120000 rw-p 00000000 00:00 0 [heap]
7f144c052000-7f144c20c000 r-xp 00000000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f144c20c000-7f144c40c000 ---p 001ba000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f144c40c000-7f144c410000 r--p 001ba000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f144c410000-7f144c412000 rw-p 001be000 08:01 136253 /lib/x86_64-linux-gnu/libc-2.19.so
7f144c412000-7f144c417000 rw-p 00000000 00:00 0
7f144c417000-7f144c43a000 r-xp 00000000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f144c61e000-7f144c621000 rw-p 00000000 00:00 0
7f144c636000-7f144c639000 rw-p 00000000 00:00 0
7f144c639000-7f144c63a000 r--p 00022000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f144c63a000-7f144c63b000 rw-p 00023000 08:01 136229 /lib/x86_64-linux-gnu/ld-2.19.so
7f144c63b000-7f144c63c000 rw-p 00000000 00:00 0
7ffc94272000-7ffc94293000 rw-p 00000000 00:00 0 [stack]
7ffc9435e000-7ffc94360000 r--p 00000000 00:00 0 [vvar]
7ffc94360000-7ffc94362000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
回想前面的内容,可以看到,栈([stack])位于内存的高地址,堆([heap])位于内存的低地址。
[heap]
从maps文件中, 我们可以找到搜索字符串需要的所有信息:
010ff000-01120000 rw-p 00000000 00:00 0 [heap]
这个进程的堆信息如下:
- 在虚拟内存中的起始地址是
0x010ff000 - 结束地址是
01120000 - 权限是可读写的(
rw)
回顾下正在运行的loop的输出:
...
[1024] Holberton (0x10ff010)
...
0x010ff000 < 0x10ff010 < 0x01120000。这证明了我们的字符串是在堆上。更精确的说,新字符串是在堆偏移0x10的地方。如果我们打开文件/proc/4618/mem并且将文件指针移动到0x10ff010, 我们就能替换正在运行的程序loop中的字符串Holberton。
我们接下来会写个程序/脚本做这件事情。
你也可以暂停阅读本文,用自己最熟悉的语言尝试写个脚本/程序来做这件事情。
.
.
.
替换虚拟内存中的字符串###
下面是我们用Python3 实现字符串替换的脚本(read_write_heap.py):
#!/usr/bin/env python3
'''
Locates and replaces the first occurrence of a string in the heap
of a process
Usage: ./read_write_heap.py PID search_string replace_by_string
Where:
- PID is the pid of the target process
- search_string is the ASCII string you are looking to overwrite
- replace_by_string is the ASCII string you want to replace
search_string with
'''
import sys
def print_usage_and_exit():
print('Usage: {} pid search write'.format(sys.argv[0]))
sys.exit(1)
# check usage
if len(sys.argv) != 4:
print_usage_and_exit()
# get the pid from args
pid = int(sys.argv[1])
if pid <= 0:
print_usage_and_exit()
search_string = str(sys.argv[2])
if search_string == "":
print_usage_and_exit()
write_string = str(sys.argv[3])
if search_string == "":
print_usage_and_exit()
# open the maps and mem files of the process
maps_filename = "/proc/{}/maps".format(pid)
print("[*] maps: {}".format(maps_filename))
mem_filename = "/proc/{}/mem".format(pid)
print("[*] mem: {}".format(mem_filename))
# try opening the maps file
try:
maps_file = open('/proc/{}/maps'.format(pid), 'r')
except IOError as e:
print("[ERROR] Can not open file {}:".format(maps_filename))
print(" I/O error({}): {}".format(e.errno, e.strerror))
sys.exit(1)
for line in maps_file:
sline = line.split(' ')
# check if we found the heap
if sline[-1][:-1] != "[heap]":
continue
print("[*] Found [heap]:")
# parse line
addr = sline[0]
perm = sline[1]
offset = sline[2]
device = sline[3]
inode = sline[4]
pathname = sline[-1][:-1]
print("\tpathname = {}".format(pathname))
print("\taddresses = {}".format(addr))
print("\tpermisions = {}".format(perm))
print("\toffset = {}".format(offset))
print("\tinode = {}".format(inode))
# check if there is read and write permission
if perm[0] != 'r' or perm[1] != 'w':
print("[*] {} does not have read/write permission".format(pathname))
maps_file.close()
exit(0)
# get start and end of the heap in the virtual memory
addr = addr.split("-")
if len(addr) != 2: # never trust anyone, not even your OS :)
print("[*] Wrong addr format")
maps_file.close()
exit(1)
addr_start = int(addr[0], 16)
addr_end = int(addr[1], 16)
print("\tAddr start [{:x}] | end [{:x}]".format(addr_start, addr_end))
# open and read mem
try:
mem_file = open(mem_filename, 'rb+')
except IOError as e:
print("[ERROR] Can not open file {}:".format(mem_filename))
print(" I/O error({}): {}".format(e.errno, e.strerror))
maps_file.close()
exit(1)
# read heap
mem_file.seek(addr_start)
heap = mem_file.read(addr_end - addr_start)
# find string
try:
i = heap.index(bytes(search_string, "ASCII"))
except Exception:
print("Can't find '{}'".format(search_string))
maps_file.close()
mem_file.close()
exit(0)
print("[*] Found '{}' at {:x}".format(search_string, i))
# write the new string
print("[*] Writing '{}' at {:x}".format(write_string, addr_start + i))
mem_file.seek(addr_start + i)
mem_file.write(bytes(write_string, "ASCII"))
# close files
maps_file.close()
mem_file.close()
# there is only one heap in our example
break
注意:需要以root权限执行上面的脚本, 否则无法读写文件/proc/pid/mem, 即使你是进程的所有者。
运行上面的脚本:
julien@holberton:~/holberton/w/hackthevm0$ sudo ./read_write_heap.py 4618 Holberton "Fun w vm!"
[*] maps: /proc/4618/maps
[*] mem: /proc/4618/mem
[*] Found [heap]:
pathname = [heap]
addresses = 010ff000-01120000
permisions = rw-p
offset = 00000000
inode = 0
Addr start [10ff000] | end [1120000]
[*] Found 'Holberton' at 10
[*] Writing 'Fun w vm!' at 10ff010
julien@holberton:~/holberton/w/hackthevm0$
可以看到上面脚本打印出来的地址和我们手动找到的是一致的:
- 进程的堆位于虚拟内存的
0x010ff000~0x01120000 - 我们要找的字符串地址是
0x10ff010, 相对于堆的起始地址偏移了0x10
回过头来看下我们的loop程序,它应该会打印字符串”fun w vm!”
...
[2676] Holberton (0x10ff010)
[2677] Holberton (0x10ff010)
[2678] Holberton (0x10ff010)
[2679] Holberton (0x10ff010)
[2680] Holberton (0x10ff010)
[2681] Holberton (0x10ff010)
[2682] Fun w vm! (0x10ff010)
[2683] Fun w vm! (0x10ff010)
[2684] Fun w vm! (0x10ff010)
[2685] Fun w vm! (0x10ff010)
...

六、下节预告
下一篇文章中我们要做的事情和本章类似, 不同的是我们将访问并修改一个Python3 脚本的内存。 这做起来比较吃力, 所以我们需要了解Pyhton3 内部的一些机制。不信你可以试试,上面的脚本read_write_heap.py并不能修改Python3进程中的ASCII字符串。
七、继续阅读
- 第一篇:虚拟内存探究 – 第一篇:C strings & /proc
- 第二篇:虚拟内存探究 – 第二篇:Python 字节
- 第三篇:虚拟内存探究 – 第三篇:一步一步画虚拟内存图
- 第四篇:虚拟内存探究 – 第四篇:malloc, heap & the program break
八、原文链接
Hack The Virtual Memory: C strings & /proc
Similar Posts
[转帖]虚拟内存探究 -- 第一篇:C strings & /proc的更多相关文章
- [转帖]虚拟内存探究 -- 第二篇:Python 字节
虚拟内存探究 -- 第二篇:Python 字节 http://blog.coderhuo.tech/2017/10/15/Virtual_Memory_python_bytes/ 是真看不懂哦 ...
- [转帖]虚拟内存探究 -- 第四篇:malloc, heap & the program break
虚拟内存探究 -- 第四篇:malloc, heap & the program break http://blog.coderhuo.tech/2017/10/19/Virtual_Memo ...
- 【转帖】Mysql多维数据仓库指南 第一篇 第1章
Mysql多维数据仓库指南 第一篇基本原理 章节列表: 第1章:基本组成 第2章:维度历史 第3章:维度可加性 第4章:维度查询 本篇概述 你将运用关系数据库来实施一个维度数据仓库.事实表和维表这两 ...
- Android基础学习第一篇—Project目录结构
写在前面的话: 1. 最近在自学Android,也是边看书边写一些Demo,由于知识点越来越多,脑子越来越记不清楚,所以打算写成读书笔记,供以后查看,也算是把自己学到所理解的东西写出来,献丑,如有不对 ...
- Windows系统CPU内存网络性能统计第一篇 内存
最近翻出以前做过的Windows系统性能统计程序,这个程序可以统计系统中的CPU使用情况,内存使用情况以及网络流量.现在将其整理一下(共有三篇),希望对大家有所帮助. 目录如下: 1.<Wind ...
- vue+uni-app商城实战 | 第一篇:【有来小店】微信小程序快速开发接入Spring Cloud OAuth2认证中心完成授权登录
一. 前言 本篇通过实战来讲述如何使用uni-app快速进行商城微信小程序的开发以及小程序如何接入后台Spring Cloud微服务. 有来商城 youlai-mall 项目是一套全栈商城系统,技术栈 ...
- 前端学习 linux —— 第一篇
前端学习 linux - 第一篇 本文主要介绍"linux 发行版本"."cpu 架构"."Linux 目录结构"."vi 和 v ...
- 从0开始搭建SQL Server AlwaysOn 第一篇(配置域控)
从0开始搭建SQL Server AlwaysOn 第一篇(配置域控) 第一篇http://www.cnblogs.com/lyhabc/p/4678330.html第二篇http://www.cnb ...
- Python爬虫小白入门(四)PhatomJS+Selenium第一篇
一.前言 在上一篇博文中,我们的爬虫面临着一个问题,在爬取Unsplash网站的时候,由于网站是下拉刷新,并没有分页.所以不能够通过页码获取页面的url来分别发送网络请求.我也尝试了其他方式,比如下拉 ...
随机推荐
- javaScript事件--事件流
一.事件 事件是文档或者浏览器窗口中发生的,特定的交互瞬间. 事件是用户或浏览器自身执行的某种动作,如click,load和mouseover都是事件的名字. 事件是javaScript和DOM之间交 ...
- 【NOIP2016提高A组模拟10.15】最大化
题目 分析 枚举两个纵坐标i.j,接着表示枚举区域的上下边界, 设对于每个横坐标区域的前缀和和为\(s_l\),枚举k, 显然当\(s_k>s_l\)时,以(i,k)为左上角,(j,k)为右下角 ...
- electron启动出现短暂的白屏
mainWindow = new BrowserWindow({ height: 600, width: 960, frame: false, minWidth: 710, minHeight: 50 ...
- 对vue虚拟dom的研究
Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象. ...
- postman添加测试
我们有的时候可能需要登陆才能使用下面的接口 解决方案.在浏览器中找到cookie.然后放在postman中的Headers中
- ThoughtWorks.QRCode 生成二维码名片(实现二维码内容换行)
最近在写一个很简单的功能,按照Vcard的格式,生成二维码名片.本来以为分分钟完事的事情,替换数据,直接调用dll去生成二维码. 测试时,发现生成的二维码使用微信扫描得到的名片信息为空,反向解析发现, ...
- (52)LINUX应用编程和网络编程之七Linux线程
3.7.1.再论进程 3.7.1.1.多进程实现同时读取键盘和鼠标 3.7.1.2.使用进程技术的优势 (1)CPU时分复用,单核心CPU可以实现宏观上的并行.微观上的串行 (2)实现多任务系统需求( ...
- arguments详解——函数内命名参数之映射
首先,arguments对象是所有(非箭头)函数中都可用的局部变量.你可以使用arguments对象在函数中引用函数的参数.此对象包含传递给函数的每个参数,第一个参数在索引0处. arguments对 ...
- SQL server 从创建数据库到查询数据的简单操作
目录. 创建数据库 创建表 插入数据 查询 1.创建数据库 --创建数据库 create database db_Product go --使用数据库use db_Productgo 2.创建表 -- ...
- vue基于element-ui的三级CheckBox复选框
最近vue项目需要用到三级CheckBox复选框,需要实现全选反选不确定三种状态.但是element-ui table只支持多选行,并不能支持三级及以上的多选,所以写了这篇技术博文供以后学习使用. 效 ...