1. 简介
使用ptrace向已运行进程中注入.so并执行相关函数,其中的“注入”二字的真正含义为:此.so被link到已运行进程(以下简称为:目标进程)空间中,从而.so中的函数在目标进程空间中有对应的地址,然后通过此地址便可在目标进程中进行调用。
到底是如何注入的呢?
本文实现方案为:在目标进程中,通过dlopen把需要注入的.so加载到目标进程的空间中。
2. 如何让目标进程执行dlopen加载.so?
显然,目标进程本来是没有实现通过dlopen来加载我们想注入的.so,为了实现此功能,我们需要目标进程执行一段我们实现的代码,此段代码的功能为通过dlopen来加载一个.so。
3. 【加载.so的实现代码】
加载需要注入的.so的实现代码如下所示:
- .global _dlopen_addr_s @dlopen函数在目标进程中的地址 注:以下全局变化在C中可读写
- .global _dlopen_param1_s @dlopen参数1<.so>在目标进程中的地址
- .global _dlopen_param2_s @dlopen参数2在目标进程中的地址
- .global _dlsym_addr_s @dlsym函数在目标进程中的地址
- .global _dlsym_param2_s @dlsym参数2在目标进程中的地址,其实为函数名
- .global _dlclose_addr_s @dlcose在目标进程中的地址
- .global _inject_start_s @汇编代码段的起始地址
- .global _inject_end_s @汇编代码段的结束地址
- .global _inject_function_param_s @hook_init参数在目标进程中的地址
- .global _saved_cpsr_s @保存CPSR,以便执行完hook_init之后恢复环境
- .global _saved_r0_pc_s @保存r0-r15,以便执行完hook_init之后恢复环境
- .data
- _inject_start_s:
- @ debug loop
- 3:
- @sub r1, r1, #0
- @B 3b
- @ dlopen
- ldr r1, _dlopen_param2_s @设置dlopen第二个参数, flag
- ldr r0, _dlopen_param1_s @设置dlopen第一个参数 .so
- ldr r3, _dlopen_addr_s @设置dlopen函数
- blx r3 @执行dlopen函数,返回值位于r0中
- subs r4, r0, #0 @把dlopen的返回值soinfo保存在r4中,以方便后面dlclose使用
- beq 2f
- @dlsym
- ldr r1, _dlsym_param2_s @设置dlsym第二个参数,第一个参数已经在r0中了
- ldr r3, _dlsym_addr_s @设置dlsym函数
- blx r3 @执行dlsym函数,返回值位于r0中
- subs r3, r0, #0 @把返回值<hook_init在目标进程中的地址>保存在r3中
- beq 1f
- @call our function
- ldr r0, _inject_function_param_s @设置hook_init第一个参数
- blx r3 @执行hook_init
- subs r0, r0, #0
- beq 2f
- 1:
- @dlclose
- mov r0, r4 @把dlopen的返回值设为dlcose的第一个参数
- ldr r3, _dlclose_addr_s @设置dlclose函数
- blx r3 @执行dlclose函数
- 2:
- @restore context
- ldr r1, _saved_cpsr_s @恢复CPSR
- msr cpsr_cf, r1
- ldr sp, _saved_r0_pc_s @恢复寄存器r0-r15
- ldmfd sp, {r0-pc}
- _dlopen_addr_s: @初始化_dlopen_addr_s
- .word 0x11111111
- _dlopen_param1_s:
- .word 0x11111111
- _dlopen_param2_s:
- .word 0x2 @RTLD_GLOBAL
- _dlsym_addr_s:
- .word 0x11111111
- _dlsym_param2_s:
- .word 0x11111111
- _dlclose_addr_s:
- .word 0x11111111
- _inject_function_param_s:
- .word 0x11111111
- _saved_cpsr_s:
- .word 0x11111111
- _saved_r0_pc_s:
- .word 0x11111111
- _inject_end_s: @代码结束地址
- .space 0x400, 0 @代码段空间大小
- .end
4. 如何把【加载.so的实现代码】写入目标进程并启动执行?
为了把【加载.so的实现代码】写入目标进程,主要有以下两步操作:
1) 在目标进程中找到存放【加载.so的实现代码】的空间(通过mmap实现)
2) 把【加载.so的实现代码】写入目标进程指定的空间
3) 启动执行
4.1 在目标进程中找到存放【加载.so的实现代码】的空间
通过mmap来实现,其实现步骤如下:
1) 获取目标进程中mmap地址
2) 把mmap参数据放入r0-r3,另外两个写入目标进程sp
3) pc设置为mmap地址,lr设置为0
4) 把准备好的寄存器写入目标进程(PTRACE_SETREGS),并启动目标进程运行(PTRACE_CONT)
5) 分配的内存首地址位于r0 (PTRACE_GETREGS)
4.2 为【加载.so的实现代码】中的全局变量赋值
1) 获取目标进程中dlopen地址并赋值给_dlopen_addr_s
2) 获取目标进程中dlsym地址并赋值给_dlsym_addr_s
3) 获取目标进程中dlclose地址并赋值给_dlclose_addr_s
4) 把需要加载的.so的路径放入 汇编代码中,并获取此路径在目标进程中的地址然后赋值给_dlopen_param1_s
5) 把需要加载的.so中的hook_init放入 汇编代码中,并获取此路径在目标进程中的地址然后赋值给_dlsym_param2_s
6) 把目标进程中的cpsr保存在_saved_cpsr_s中
7) 把目标进程中的r0-r15存入汇编代码中,并获取此变量在目标进程中的地址然后赋值给_saved_r0_pc_s
8) 通过ptrace( PTRACE_POKETEXT,...)把汇编代码写入目标进程中,起始地址由前面的mmap所分配
9) 把目标进程的pc设置为汇编代码的起始地址,然后调用ptrace(PTRACE_DETACH,...)以启动目标进程执行
5. 把汇编代码写入目标进程并执行的实现代码
5.1 主函数 writecode_to_targetproc
- #include <stdio.h>
- #include <stdlib.h>
- #include <asm/ptrace.h>
- #include <asm/user.h>
- #include <asm/ptrace.h>
- #include <sys/wait.h>
- #include <sys/mman.h>
- #include <dlfcn.h>
- #include <dirent.h>
- #include <unistd.h>
- #include <string.h>
- #include <android/log.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <sys/stat.h>
- #define MAX_PATH 0x100
- #define REMOTE_ADDR( addr, local_base, remote_base ) ( (uint32_t)(addr) + (uint32_t)(remote_base) - (uint32_t)(local_base) )
- /* write the assembler code into target proc,
- * and invoke it to execute
- */
- int writecode_to_targetproc(
- pid_t target_pid, // target process pid
- const char *library_path, // the path of .so that will be
- // upload to target process
- const char *function_name, // .so init fucntion e.g. hook_init
- void *param, // the parameters of init function
- size_t param_size ) // number of parameters
- {
- int ret = -1;
- void *mmap_addr, *dlopen_addr, *dlsym_addr, *dlclose_addr;
- void *local_handle, *remote_handle, *dlhandle;
- uint8_t *map_base;
- uint8_t *dlopen_param1_ptr, *dlsym_param2_ptr, *saved_r0_pc_ptr, *inject_param_ptr, *remote_code_ptr, *local_code_ptr;
- struct pt_regs regs, original_regs;
- // extern global variable in the assembler code
- extern uint32_t _dlopen_addr_s, _dlopen_param1_s, _dlopen_param2_s, \
- _dlsym_addr_s, _dlsym_param2_s, _dlclose_addr_s, \
- _inject_start_s, _inject_end_s, _inject_function_param_s, \
- _saved_cpsr_s, _saved_r0_pc_s;
- uint32_t code_length;
- long parameters[10];
- // make target_pid as its child process and stop
- if ( ptrace_attach( target_pid ) == -1 )
- return -1;
- // get the values of 18 registers from target_pid
- if ( ptrace_getregs( target_pid, ®s ) == -1 )
- goto exit;
- // save original registers
- memcpy( &original_regs, ®s, sizeof(regs) );
- // get mmap address from target_pid
- // the mmap is the address of mmap in the cur process
- mmap_addr = get_remote_addr( target_pid, "/system/lib/libc.so", (void *)mmap );
- // set mmap parameters
- parameters[0] = 0; // addr
- parameters[1] = 0x4000; // size
- parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // prot
- parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flags
- parameters[4] = 0; //fd
- parameters[5] = 0; //offset
- // execute the mmap in target_pid
- if ( ptrace_call( target_pid, (uint32_t)mmap_addr, parameters, 6, ®s ) == -1 )
- goto exit;
- // get the return values of mmap <in r0>
- if ( ptrace_getregs( target_pid, ®s ) == -1 )
- goto exit;
- // get the start address for assembler code
- map_base = (uint8_t *)regs.ARM_r0;
- // get the address of dlopen, dlsym and dlclose in target process
- dlopen_addr = get_remote_addr( target_pid, "/system/bin/linker", (void *)dlopen );
- dlsym_addr = get_remote_addr( target_pid, "/system/bin/linker", (void *)dlsym );
- dlclose_addr = get_remote_addr( target_pid, "/system/bin/linker", (void *)dlclose );
- // set the start address for assembler code in target process
- remote_code_ptr = map_base + 0x3C00;
- // set the start address for assembler code in cur process
- local_code_ptr = (uint8_t *)&_inject_start_s;
- // set global variable of assembler code
- // and these address is in the target process
- _dlopen_addr_s = (uint32_t)dlopen_addr;
- _dlsym_addr_s = (uint32_t)dlsym_addr;
- _dlclose_addr_s = (uint32_t)dlclose_addr;
- code_length = (uint32_t)&_inject_end_s - (uint32_t)&_inject_start_s;
- dlopen_param1_ptr = local_code_ptr + code_length + 0x20;
- dlsym_param2_ptr = dlopen_param1_ptr + MAX_PATH;
- saved_r0_pc_ptr = dlsym_param2_ptr + MAX_PATH;
- inject_param_ptr = saved_r0_pc_ptr + MAX_PATH;
- // save library path to assembler code global variable
- strcpy( dlopen_param1_ptr, library_path );
- _dlopen_param1_s = REMOTE_ADDR( dlopen_param1_ptr, local_code_ptr, remote_code_ptr );
- // save function name to assembler code global variable
- strcpy( dlsym_param2_ptr, function_name );
- _dlsym_param2_s = REMOTE_ADDR( dlsym_param2_ptr, local_code_ptr, remote_code_ptr );
- // save cpsr to assembler code global variable
- _saved_cpsr_s = original_regs.ARM_cpsr;
- // save r0-r15 to assembler code global variable
- memcpy( saved_r0_pc_ptr, &(original_regs.ARM_r0), 16 * 4 ); // r0 ~ r15
- _saved_r0_pc_s = REMOTE_ADDR( saved_r0_pc_ptr, local_code_ptr, remote_code_ptr );
- // save function parameters to assembler code global variable
- memcpy( inject_param_ptr, param, param_size );
- _inject_function_param_s = REMOTE_ADDR( inject_param_ptr, local_code_ptr, remote_code_ptr );
- // write the assembler code into target process
- // now the values of global variable is in the target process space
- ptrace_writedata( target_pid, remote_code_ptr, local_code_ptr, 0x400 );
- memcpy( ®s, &original_regs, sizeof(regs) );
- // set sp and pc to the start address of assembler code
- regs.ARM_sp = (long)remote_code_ptr;
- regs.ARM_pc = (long)remote_code_ptr;
- // set registers for target process
- ptrace_setregs( target_pid, ®s );
- // make the target_pid is not a child process of cur process
- // and make target_pid continue to running
- ptrace_detach( target_pid );
- // now finish it successfully
- ret = 0;
- exit:
- return ret;
- }
5.2 attach目标进程ptrace_attach
- int ptrace_attach( pid_t pid )
- {
- // after PTRACE_ATTACH, the proc<pid> will stop
- if ( ptrace( PTRACE_ATTACH, pid, NULL, 0 ) < 0 )
- {
- perror( "ptrace_attach" );
- return -1;
- }
- // wait proc<pid> stop
- waitpid( pid, NULL, WUNTRACED );
- // after PTRACE_SYSCALL, the proc<pid> will continue,
- // but when exectue sys call function, proc<pid> will stop
- if ( ptrace( PTRACE_SYSCALL, pid, NULL, 0 ) < 0 )
- {
- perror( "ptrace_syscall" );
- return -1;
- }
- // wait proc<pid> stop
- waitpid( pid, NULL, WUNTRACED );
- return 0;
- }
5.3 获取目标进程寄存器值ptrace_getregs
- int ptrace_getregs( pid_t pid, struct pt_regs* regs )
- {
- if ( ptrace( PTRACE_GETREGS, pid, NULL, regs ) < 0 )
- {
- perror( "ptrace_getregs: Can not get register values" );
- return -1;
- }
- return 0;
- }
5.4 获取目标进程中指定模块中指定函数的地址get_remote_addr
- /* find the start address of module whose name is module_name
- * in the designated process
- */
- void* get_module_base( pid_t pid, const char* module_name )
- {
- FILE *fp;
- long addr = 0;
- char *pch;
- char filename[32];
- char line[1024];
- if ( pid < 0 )
- {
- /* self process */
- snprintf( filename, sizeof(filename), "/proc/self/maps", pid );
- }
- else
- {
- snprintf( filename, sizeof(filename), "/proc/%d/maps", pid );
- }
- fp = fopen( filename, "r" );
- if ( fp != NULL )
- {
- while ( fgets( line, sizeof(line), fp ) )
- {
- if ( strstr( line, module_name ) )
- {
- pch = strtok( line, "-" );
- addr = strtoul( pch, NULL, 16 );
- if ( addr == 0x8000 )
- addr = 0;
- break;
- }
- }
- fclose( fp ) ;
- }
- return (void *)addr;
- }
- void* get_remote_addr( pid_t target_pid, const char* module_name, void* local_addr )
- {
- void* local_handle, *remote_handle;
- local_handle = get_module_base( -1, module_name );
- remote_handle = get_module_base( target_pid, module_name );
- return (void *)( (uint32_t)local_addr + (uint32_t)remote_handle - (uint32_t)local_handle );
- }
5.5 在目标进程中执行指定函数ptrace_call
- int ptrace_call( pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs )
- {
- uint32_t i;
- // put the first 4 parameters into r0-r3
- for ( i = 0; i < num_params && i < 4; i ++ )
- {
- regs->uregs[i] = params[i];
- }
- // push remained params into stack
- if ( i < num_params )
- {
- regs->ARM_sp -= (num_params - i) * sizeof(long) ;
- ptrace_writedata( pid, (void *)regs->ARM_sp, (uint8_t *)¶ms[i], (num_params - i) * sizeof(long) );
- }
- // set the pc to func <e.g: mmap> that will be executed
- regs->ARM_pc = addr;
- if ( regs->ARM_pc & 1 )
- {
- /* thumb */
- regs->ARM_pc &= (~1u);
- regs->ARM_cpsr |= CPSR_T_MASK;
- }
- else
- {
- /* arm */
- regs->ARM_cpsr &= ~CPSR_T_MASK;
- }
- // when finish this func, pid will stop
- regs->ARM_lr = 0;
- // set the regsister and start to execute
- if ( ptrace_setregs( pid, regs ) == -1
- || ptrace_continue( pid ) == -1 )
- {
- return -1;
- }
- // wait pid finish work and stop
- waitpid( pid, NULL, WUNTRACED );
- return 0;
- }
5.6 把代码写入目标进程指定地址ptrace_writedata
- int ptrace_writedata( pid_t pid, uint8_t *dest, uint8_t *data, size_t size )
- {
- uint32_t i, j, remain;
- uint8_t *laddr;
- union u {
- long val;
- char chars[sizeof(long)];
- } d;
- j = size / 4;
- remain = size % 4;
- laddr = data;
- for ( i = 0; i < j; i ++ )
- {
- memcpy( d.chars, laddr, 4 );
- ptrace( PTRACE_POKETEXT, pid, dest, d.val );
- dest += 4;
- laddr += 4;
- }
- if ( remain > 0 )
- {
- d.val = ptrace( PTRACE_PEEKTEXT, pid, dest, 0 );
- for ( i = 0; i < remain; i ++ )
- {
- d.chars[i] = *laddr ++;
- }
- ptrace( PTRACE_POKETEXT, pid, dest, d.val );
- }
- return 0;
- }
5.7 设置目标进程寄存器ptrace_setregs
- int ptrace_setregs( pid_t pid, struct pt_regs* regs )
- {
- if ( ptrace( PTRACE_SETREGS, pid, NULL, regs ) < 0 )
- {
- perror( "ptrace_setregs: Can not set register values" );
- return -1;
- }
- return 0;
- }
5.8 detach目标进程ptrace_detach
- int ptrace_detach( pid_t pid )
- {
- if ( ptrace( PTRACE_DETACH, pid, NULL, 0 ) < 0 )
- {
- perror( "ptrace_detach" );
- return -1;
- }
- return 0;
- }
6. 需要被加载的.so
需要被加载的.so例子程序如下,其目的是替换目标进程libapp.so中的strlen函数。其主要实现见hook_init。
- int g_isInit = 0;
- pthread_t g_hThread;
- __attribute__((visibility("default"))) void hook_init( char *args )
- {
- if( g_isInit == 1 )
- {
- printf("i am already in!");
- return;
- }
- void* soHandle = NULL;
- // the libapp.so is a .so of target process, and it call strcmp
- soHandle = dlopen( "libapp.so", RTLD_GLOBAL );
- if( soHandle != NULL )
- {
- g_realstrcmp = NULL;
- replaceFunc( soHandle, "strcmp", my_strcmp, (void**)&g_realstrcmp );
- int ret = pthread_create( &g_hThread, NULL, my_thread, NULL );
- if( ret != 0 )
- {
- printf("create thread error:%d", ret );
- }
- g_isInit = 1;
- }
- }
6.1 替换函数replaceFunc
- // replace function of libapp.so
- // e.g: replace strcmp of libapp.so with my_strcmp
- void replaceFunc(void *handle,const char *name, void* pNewFun, void** pOldFun )
- {
- if(!handle)
- return;
- soinfo *si = (soinfo*)handle;
- Elf32_Sym *symtab = si->symtab;
- const char *strtab = si->strtab;
- Elf32_Rel *rel = si->plt_rel;
- unsigned count = si->plt_rel_count;
- unsigned idx;
- // these external functions that are called by libapp.so
- // is in the plt_rel
- for(idx=0; idx<count; idx++)
- {
- unsigned type = ELF32_R_TYPE(rel->r_info);
- unsigned sym = ELF32_R_SYM(rel->r_info);
- unsigned reloc = (unsigned)(rel->r_offset + si->base);
- char *sym_name = (char *)(strtab + symtab[sym].st_name);
- if(strcmp(sym_name, name)==0)
- {
- *pOldFun = (void *)*((unsigned*)reloc);
- *((unsigned*)reloc)= pNewFun;
- break;
- }
- rel++;
- }
- }
6.2 新函数及其它函数
- // global function variable, save the address of strcmp of libapp.so
- int (*g_realstrcmp)(const char *s1, const char *s2);
- // my strcmp function
- int my_strcmp(const char *s1, const char *s2)
- {
- if( g_realstrcmp != NULL )
- {
- int nRet = g_realstrcmp( s1, s2 );
- printf("***%s: s1=%s, s2=%s\n",__FUNCTION__, s1, s2 );
- return nRet;
- }
- return -1;
- }
- // create a thread
- void* my_thread( void* pVoid )
- {
- int sock;
- sock = socket(AF_INET, SOCK_DGRAM, 0);
- if( sock < -1 )
- {
- printf("create socket failed!\n");
- return 0;
- }
- struct sockaddr_in addr_serv;
- int len;
- memset(&addr_serv, 0, sizeof(struct sockaddr_in));
- addr_serv.sin_family = AF_INET;
- addr_serv.sin_port = htons(9999);
- addr_serv.sin_addr.s_addr = inet_addr("127.0.0.1");
- len = sizeof(addr_serv);
- int flags = fcntl( sock, F_GETFL, 0);
- fcntl( sock, F_SETFL, flags | O_NONBLOCK);
- int nPreState = -1;
- unsigned char data=0;
- while( 1 )
- {
- data++;
- sendto( sock, &data, sizeof( data ), 0, (struct sockaddr *)&addr_serv, sizeof( addr_serv ) );
- usleep( 30000 );
- }
- }
使用ptrace向已运行进程中注入.so并执行相关函数的更多相关文章
-
使用ptrace向已运行进程中注入.so并执行相关函数(转)
1. 简介 使用ptrace向已运行进程中注入.so并执行相关函数,其中的“注入”二字的真正含义为:此.so被link到已运行进程(以下简称为:目标进程)空间中,从而.so中的函数在目标进程空间中有对 ...
-
Linux查询已开启文件或已运行进程开启之文件fuser,lsof,pidof
fuser:藉由文件(或文件系统)找出正在使用该文件的程序 [root@www ~]# fuser [-umv] [-k [i] [-signal]] file/dir 选项与参数: -u :除了进程 ...
-
2.添加键盘钩子。向进程中注入dll
学习笔记 1.首先要建立mfc的动态链接库.在def文件中放入要导出的函数名. 2.添加函数如下 //安装钩子 //HHOOK SetWindowsHookEx( // int idHook,//钩子 ...
-
将dll文件注入到其他进程中的一种新方法
http://www.45it.com/windowszh/201212/33946.htm http://www.hx95.cn/Article/OS/201212/65095.html 我们知道将 ...
-
INNO setup安装卸载钱判断进程中是否在运行总结
1.安装前判断进程中是否有程序在运行. [files] ; 安装前判断进程,dll文件放在inno的安装目录中Source: compiler:psvince.dll; Flags: dontcopy ...
-
linux中应用程序main函数中没有开辟进程的,它应该在那个进程中运行呢?
1.main函数是一个进程还是一个线程? 不知道你是用c创建的,还是用java创建的. 因为它们都是以main()做为入口开始运行的. 是一个线程,同时还是一个进程. 在现在的操作系统中,都是多线程的 ...
-
错误 : 资产文件“项目\obj\project.assets.json”没有“.NETCoreApp,Version=v2.0”的目标。确保已运行还原,且“netcoreapp2.0”已包含在项目的 TargetFrameworks 中。
升级 vs201715.6.3之后发布出现 错误 : 资产文件“项目\obj\project.assets.json”没有“.NETCoreApp,Version=v2.0”的目标.确保已运行还原,且 ...
-
pgrep---以名称为依据从运行进程队列中查找进程
pgrep命令以名称为依据从运行进程队列中查找进程,并显示查找到的进程id.每一个进程ID以一个十进制数表示,通过一个分割字符串和下一个ID分开,默认的分割字符串是一个新行.对于每个属性选项,用户可以 ...
-
在Linux中通过Top运行进程查找最高内存和CPU使用率
按内存使用情况查找前15个进程,在批处理模式下为"top" 使用top命令查看有关当前状态,系统使用情况的更详细信息:正常运行时间,负载平均值和进程总数. 分类:Linux命令操作 ...
随机推荐
-
window.print() 打印页面部分内容的方法
用 JavaScript 实现页面数据的打印 : 主要是用到了一个 print() 函数 , 该函数将会打印整个 web 页面 body 内的所有 html 数据 ! 使用方法为 window.pri ...
-
ahjesus自定义隐式转换和显示转换
implicit 关键字用于声明隐式的用户定义类型转换运算符. 如果可以确保转换过程不会造成数据丢失,则可使用该关键字在用户定义类型和其他类型之间进行隐式转换. 参考戳此 explicit ...
-
Linux系统下设置Tomcat自启动
需要将tomcat加入自启动队列中,则需要进行如下的操作: 以root用户登录系统: cd /etc/rc.d/init.d/ vi tomcat 文件内容参考如下: #!/bin/sh # # to ...
-
Memcache技术分享:介绍、使用、存储、算法、优化、命中率
1.memcached 介绍 1.1 memcached 是什么? memcached 是以LiveJournal旗下Danga Interactive 公司的Brad Fitzpatric 为首开发 ...
-
【Linux常识篇(1)】所谓的正向代理与反向代理
正向代理的概念 正向代理,也就是传说中的代理,他的工作原理就像一个跳板,简单的说,我是一个用户,我访问不了某网站,但是我能访问一个代理服务器,这个代理服务器呢,他能访问那个我不能访问的网站,于是我先连 ...
-
学习LINQ,发现一个好的工具。LINQPad!!
今日学习LINQ,发现一个好的工具.LINQPad!! 此工具的好处在于,不需要在程序内执行,直接只用工具测试.然后代码通过即可,速度快,简洁方便. 可以生成其LINQ查询对应的lambda和SQL语 ...
-
游戏开发设计模式之子类沙盒模式(unity3d 示例实现)
积累提供所有操作(的实现)来定义子类的行为用一个最简单的例子来讲解这个模式玩家操纵的英雄也就是这个游戏的主角会有许多技能,我们想定义许多不同的技能,来让玩家使用.首 先我们定义一个skillBase类 ...
-
用Spring Boot零配置快速创建web项目(1)
一.Spring Boot简介 Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人 ...
-
JS Bootstrap-DateRangePicker 如何设置默认值为空
DateRangePicker是一款时间范围选择器,界面良好,非常适合短时间范围选择的插件,具体源码可以在http://www.daterangepicker.com/找到 ,但是目前使用中,感觉功能 ...
-
前端开发模拟数据------webpack-api-mocker
应用场景: 在实际的项目开发过程中,一般都会进行前后端分离的开发模式,前端通过mock或者其他的插件模拟后台返回数据的功能.在常用的webpack构建工程项目中,通过和webpack-dev-serv ...