在了解内核之前,先要会编译、调试内核。

编译内核

安装依赖:

sudo apt-get update

sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc

解压后进入目录,执行下面命令进行配置:

make menuconfig

配置的时候基本什么都不需要改动,直接Save,然后Exit。

然后运行下面的命令进行内核编译,该过程会花费较长时间。

make bzImage

编译好之后,在./arch/x86/boot/拿到bzImage,从源码根目录拿到vmlinux。

Setup is 17628 bytes (padded to 17920 bytes).

System is 8485 kB

CRC 7bdf0988

Kernel: arch/x86/boot/bzImage is ready (#1)

添加自定义syscall

在源码根目录创建一个新的目录(模块),以经典的helloworld为例。

ex@Ex:~/test/temp/linux-5.1.7$ cd helloworld/

ex@Ex:~/test/temp/linux-5.1.7/helloworld$ tree

.

├── helloworld.c

└── Makefile

0 directories, 2 files

ex@Ex:~/test/temp/linux-5.1.7/helloworld$ cat helloworld.c

#include

asmlinkage long sys_helloworld(void){

printk("hello world\n");

return 0;

}

ex@Ex:~/test/temp/linux-5.1.7/helloworld$ cat Makefile

obj-y=helloworld.o

编辑源码根目录下的Makefile,加入helloworld模块。

...

PHONY += prepare0

ifeq ($(KBUILD_EXTMOD),)

core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/ helloworld/

vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \

$(core-y) $(core-m) $(drivers-y) $(drivers-m) \

$(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y)))

vmlinux-alldirs := $(sort $(vmlinux-dirs) $(patsubst %/,%,$(filter %/, \

$(init-) $(core-) $(drivers-) $(net-) $(libs-) $(virt-))))

...

然后编辑include/linux/syscalls.h,添加helloworld函数原型。

asmlinkage long sys_helloworld(void);

增加在文件末尾即可。

然后再修改arch/x86/entry/syscalls/syscall_32.tbl和arch/x86/entry/syscalls/syscall_64.tbl,添加自定义的系统调用号。

i386:

1000 i386 helloworld sys_helloworld

amd64:

1000 common helloworld sys_helloworld

最后在编译生成新的内核即可。

编译busybox

下载完成后解压进入源码根目录输入make menuconfig进行配置。

最好在配置时进入Settings,勾上Build static binary (no shared libs),这样就不会依赖libc文件。

ex@Ex:~/test/temp/busybox-1.31.0/_install$ ldd bin/busybox

not a dynamic executable

如果不勾选的话,需要自行配置libc库,这样步骤会很繁琐。

然后输入make install -j4进行编译,busybox编译要比kernel快很多。

编译完成后会生成一个_install的目录,这就是我们需要的环境。

先进行一些简单的初始化:

cd _install

mkdir proc

mkdir sys

mkdir lib64

mkdir -p lib/x86_64-linux-gnu/

mkdir etc

mkdir home

echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd

echo "root:x:0:" > etc/group

touch etc/shadow

touch etc/gshadow

touch init

chmod +x init

然后把libc和ld准备好,否则程序需要静态编译才能运行,则会使得生成的程序调试的时候不太方便。

在生成的init初始化脚本中,加入如下内容:

#!/bin/sh

echo "{==DBG==} INIT SCRIPT"

mkdir /tmp

mount -t proc none /proc

mount -t sysfs none /sys

mount -t debugfs none /sys/kernel/debug

mount -t tmpfs none /tmp

mount -t devtmpfs devtmpfs /dev

# insmod /xxx.ko # load ko

mdev -s # We need this to find /dev/sda later

echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"

setsid /bin/cttyhack setuidgid 1000 /bin/sh #normal user

# exec /bin/sh #root

poweroff -d 0 -f

然后在_install目录里运行下面的命令进行打包:

find . | cpio -o --format=newc > ../rootfs.img

qemu

通过上面两步,我们得到了含有helloworld syscall的kernel bzImage和用busybox打包的fs(附带了ld和libc)。

接下来只要用qemu启动就ok了。

在这之前,可以先写一个测试程序来测试我们写的syscall。

// compiled: gcc helloworld.c -o helloworld

#include

#include

int main()

{

puts("start");

syscall(1000);

puts("end");

return 0;

}

将生成可执行二进制文件helloworld放在_install目录下,重新进行打包。

然后在用qemu启动:

qemu-system-x86_64 -cpu kvm64,+smep -kernel ./bzImage -initrd rootfs.img -nographic -append "console=ttyS0"

运行实例:

/ $ id

uid=1000 gid=1000 groups=1000

/ $ ./helloworld

start

[ 29.085005] hello world

end

/ $

驱动

register_chrdev

int register_chrdev (unsigned int major, const char *name, struct file_operations*fops);

在这里,我们指定要注册它的设备的名称和主要编号,之后将链接设备和file_operations结构。 如果我们为主参数指定零,该函数将自己分配一个主设备号(即它返回的值)。 如果返回的值为零,则表示成功,而负数表示错误。 两个设备编号均在0-255范围内指定。

我们将设备名称作为name参数的字符串值传递(如果模块注册单个设备,则此字符串也可以传递模块的名称)。 然后,我们使用此字符串来标识/sys/devices文件中的设备。 读取,写入和保存等设备文件操作由存储在file_operations结构中的函数指针处理。 这些函数由模块实现,并且指向标识该模块的module结构的指针也存储在file_operations结构中。

来自源码:linux-5.2.7/include/linux/fs.h:1791

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 (*iopoll)(struct kiocb *kiocb, bool spin);

int (*iterate) (struct file *, struct dir_context *);

int (*iterate_shared) (struct file *, struct dir_context *);

__poll_t (*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 *);

unsigned long mmap_supported_flags;

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);

loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,

struct file *file_out, loff_t pos_out,

loff_t len, unsigned int remap_flags);

int (*fadvise)(struct file *, loff_t, loff_t, int);

} __randomize_layout;

如果file_operations结构包含一些不需要的函数,您仍然可以使用该文件而不实现它们。 指向未实现函数的指针可以简单地设置为零。 之后,系统将负责该功能的实现并使其正常运行。

字符设备模块使用insmod加载,加载完毕需要在/dev目录下使用mkmod命令建立相应的文件结点

编写驱动程序:

memory.c

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

MODULE_LICENSE("Dual BSD/GPL");

int memory_open(struct inode *inode, struct file *filp);

int memory_release(struct inode *inode, struct file *filp);

ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);

ssize_t memory_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos);

void memory_exit(void);

int memory_init(void);

struct file_operations memory_fops = {

read : memory_read,

write : memory_write,

open : memory_open,

release : memory_release

};

module_init(memory_init);

module_exit(memory_exit);

int memory_major = 60;

char *memory_buffer;

int used = 0;

#define LENGTH 0x1000

int memory_init(void)

{

int result;

result = register_chrdev(memory_major, "memory", &memory_fops);

if (result < 0)

{

printk("<1>memory: can't obtain major number %d\n", memory_major);

return result;

}

memory_buffer = kmalloc(LENGTH, GFP_KERNEL);

if (!memory_buffer)

{

result = -ENOMEM;

goto fail;

}

memset(memory_buffer, 0, LENGTH);

printk("<1>Inserting memory module\n");

return 0;

fail:

memory_exit();

return result;

}

void memory_exit(void)

{

unregister_chrdev(memory_major, "memory");

if (memory_buffer)

kfree(memory_buffer);

printk("<1>Removing memory module\n");

}

int memory_open(struct inode *inode, struct file *filp)

{

printk("<1>Open\n");

return 0;

}

int memory_release(struct inode *inode, struct file *filp)

{

printk("<1>Release\n");

return 0;

}

ssize_t memory_read(struct file *filp, char *buf,

size_t count, loff_t *f_pos)

{

int bytes;

if(used > count && used > 0)

{

used -= count;

bytes = count;

copy_to_user(buf, memory_buffer, bytes);

}

else if(used > 0)

{

bytes = used;

used = 0;

copy_to_user(buf, memory_buffer, bytes);

}

return bytes;

}

ssize_t memory_write(struct file *filp, const char *buf,

size_t count, loff_t *f_pos)

{

int bytes = 0;

if(used + count < LENGTH)

{

used += count;

bytes = count;

copy_from_user(memory_buffer, buf, bytes);

}

else if(used < LENGTH)

{

bytes = LENGTH - used;

used = LENGTH;

copy_from_user(memory_buffer, buf, bytes);

}

return bytes;

}

上面的驱动可以看成一个简单的字符仓库,如果放满了字符就放不进去,如果是空的也拿不出来。

驱动源码并不能用gcc直接进行编译,需要生成一个Makefile来进行编译。

TARGET_MODULE:=memorys

PWD:=$(shell pwd)

# KERNELDIR := /lib/modules/$(shell uname -r)/build

KERNELDIR:=./linux-4.15

$(TARGET_MODULE)-objs := memory.o

obj-m := $(TARGET_MODULE).o

all:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:

rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.order *.symvers

对应的内核要编译相对应的驱动才能载入,否则会失败。

编译好会生成一个memorys.ko的驱动。

这时我们可以把驱动复制到_install根目录,然后在我们的init脚本中加入下面两条命令,重新生成镜像。

insmod /memorys.ko

mknod /dev/memorys c 60 0

60 为我们设置的主设备号

运行实例:

/ # ls

bin lib memorys.ko sbin usr

dev lib64 proc sys

init linuxrc root tmp

/ # echo 1234567890 > /dev/memorys

[ 25.850176] <1>Open

[ 25.855288] <1>Release

/ # cat /dev/memorys

[ 31.262535] <1>Open

1234567890

[ 31.266417] <1>Release

/ #

上面的编译方式是早起驱动开发常用的。

根据新的资料,我重新编写了一个自动挂载的驱动,代码如下:

test_src.c

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

MODULE_LICENSE("Dual BSD/GPL");

int test_open(struct inode *inode, struct file *filp);

int test_release(struct inode *inode, struct file *filp);

ssize_t test_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);

ssize_t test_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos);

long test_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);

void test_exit(void);

int test_init(void);

struct file_operations test_fops = {

.read = test_read,

.write = test_write,

.open = test_open,

.release = test_release,

.unlocked_ioctl = test_ioctl,

.owner = THIS_MODULE

};

module_init(test_init);

module_exit(test_exit);

char *test_buffer;

int used = 0;

#define LENGTH 0x1000

dev_t test_major;

struct cdev test_cdev;

struct class *test_class;

int test_init(void)

{

int result;

if(alloc_chrdev_region(&test_major, 0, 1, "test") >= 0)

{

cdev_init(&test_cdev, &test_fops);

test_cdev.owner = THIS_MODULE;

result = cdev_add(&test_cdev, test_major, 1);

if(result)

{

printk(KERN_ERR "cedv_add error\n");

unregister_chrdev_region(test_major, 1LL);

return -1;

}

else

{

test_class = class_create(THIS_MODULE, "test");

if(test_class)

{

result = device_create(test_class, NULL, test_major, NULL, "test");

if(result)

{

printk(KERN_INFO "Register success\n");

test_buffer = kmalloc(LENGTH, GFP_KERNEL);

return 0;

}

else

{

printk(KERN_ERR "device_create error\n");

class_destroy(test_class);

cdev_del(&test_cdev);

unregister_chrdev_region(test_major, 1LL);

return -1;

}

}

else

{

printk(KERN_ERR "class_create error\n");

cdev_del(&test_cdev);

unregister_chrdev_region(test_major, 1LL);

return -1;

}

}

}

}

void test_exit(void)

{

device_destroy(test_class, test_major);

class_destroy(test_class);

cdev_del(&test_cdev);

unregister_chrdev_region(test_major, 1LL);

unregister_chrdev(test_major, "test");

if (test_buffer)

kfree(test_buffer);

printk(KERN_INFO "Removing test module\n");

}

int open_times = 0;

int release_times = 0;

long test_ioctl (struct file *filp, unsigned int cmd, unsigned long arg)

{

return 0;

}

int test_open(struct inode *inode, struct file *filp)

{

printk(KERN_INFO "Open %d times\n", release_times);

open_times++;

return 0;

}

int test_release(struct inode *inode, struct file *filp)

{

printk(KERN_INFO "Release %d times\n", release_times);

release_times++;

return 0;

}

ssize_t test_read(struct file *filp, char *buf,

size_t count, loff_t *f_pos)

{

int bytes;

if (used > count && used > 0)

{

used -= count;

bytes = count;

copy_to_user(buf, test_buffer, bytes);

}

else if (used > 0)

{

bytes = used;

used = 0;

copy_to_user(buf, test_buffer, bytes);

}

return bytes;

}

ssize_t test_write(struct file *filp, const char *buf,

size_t count, loff_t *f_pos)

{

int bytes = 0;

if (used + count < LENGTH)

{

used += count;

bytes = count;

copy_from_user(test_buffer, buf, bytes);

}

else if (used < LENGTH)

{

bytes = LENGTH - used;

used = LENGTH;

copy_from_user(test_buffer, buf, bytes);

}

return bytes;

}

其对应的Makefile如下:

TARGET_MODULE:=test

PWD:=$(shell pwd)

# KERNELDIR := /lib/modules/$(shell uname -r)/build

KERNELDIR:=./linux-5.2.7

$(TARGET_MODULE)-objs := test_src.o

obj-m := $(TARGET_MODULE).o

all:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:

rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.order *.symvers

调试

一般来说加nokaslr把kaslr关了调试起来会方便一些。否则gdb将找不到ELF基地址(毕竟不是本地)。

-append "console=ttyS0 nokaslr"

但是调试驱动时,即使关闭了kalsr,gdb也无法确定其基地址,这时候我们需要用add-symbol-file来手动添加基地址。

下面我写了一个程序方便快速读取驱动的基地址信息:

vmmap.c

// musl-gcc -static vmmap.c -O3 -s -o vmmap

#include

#include

#include

#include

#include

inline static void truncate_string(char *str)

{

while(!(*str == '\n' || *str == '\0'))

{

str++;

}

if(*str == '\n')

{

*str = '\0';

}

}

int main(int argc, char const *argv[])

{

DIR * dir;

struct dirent *ptr[0x100], *temp;

int i, num = 0, text_position = -1;

FILE *fp;

char buf[0x400];

memset(ptr, 0, sizeof(ptr));

setvbuf(stdin, NULL, _IONBF, 0);

setvbuf(stdout, NULL, _IONBF, 0);

setvbuf(stderr, NULL, _IONBF, 0);

if(argc < 2)

{

fprintf(stderr, "Usage: ./vmmap file-path\n");

exit(1);

}

if(chdir(argv[1]) == -1)

{

fprintf(stderr, "chdir error: %m\n");

exit(1);

}

dir = opendir(argv[1]);

if(dir == NULL)

{

fprintf(stderr, "opendir error: %m\n");

exit(1);

}

for(i = 0; i < 0x100; i++)

{

temp = readdir(dir);

if(temp == NULL)

{

break;

}

if(temp->d_type == DT_REG)

{

if(!strcmp(".text", temp->d_name))

{

text_position = num;

ptr[num] = temp;

}

else

{

ptr[num] = temp;

}

num++;

}

}

if(text_position == -1)

{

fprintf(stderr, "Error: don't find .text\n");

exit(1);

}

fp = fopen(ptr[text_position]->d_name, "rb");

if(fp == NULL)

{

fprintf(stderr, "fopen error: %m\n");

exit(1);

}

fgets(buf, 0x400, fp);

truncate_string(buf);

printf(" %s ", buf);

fclose(fp);

for(i = 0; i < num; i++)

{

if(i == text_position)

{

continue;

}

fp = fopen(ptr[i]->d_name, "rb");

if(fp == NULL)

{

fprintf(stderr, "fopen error: %m\n");

exit(1);

}

fgets(buf, 0x400, fp);

truncate_string(buf);

printf("-s %s %s ", ptr[i]->d_name, buf);

fclose(fp);

}

puts("");

return 0;

}

使用方法如下:

/ # /vmmap /sys/module/test/sections

0xffffffffc00fd000 -s .note.Linux 0xffffffffc00fe138 -s .strtab 0xffffffffc01026a8 -s __mcount_loc 0xffffffffc00fe024 -s .bss 0xffffffffc00ff480 -s .gnu.linkonce.this_module 0xffffffffc00ff140 -s .symtab 0xffffffffc0102000 -s .note.gnu.build-id 0xffffffffc00fe000 -s .data 0xffffffffc00ff000 -s __bug_table 0xffffffffc00ff100 -s .rodata.str1.1 0xffffffffc00fe05c -s .rodata.str1.8 0xffffffffc00fe110

可以写一个脚本来快速连接:

#!/bin/sh

gdb -q \

-ex "file ./vmlinux" \

-ex "add-symbol-file ./test.ko 0x.... -s .bss 0x...." \

-ex "target remote localhost:1000"

那么结合上面写的vmmap其脚本就是下面这个样子:

#!/bin/sh

gdb -q \

-ex "file ./vmlinux" \

-ex "add-symbol-file ./test.ko 0xffffffffc00fd000 -s .note.Linux 0xffffffffc00fe138 -s .strtab 0xffffffffc01026a8 -s __mcount_loc 0xffffffffc00fe024 -s .bss 0xffffffffc00ff480 -s .gnu.linkonce.this_module 0xffffffffc00ff140 -s .symtab 0xffffffffc0102000 -s .note.gnu.build-id 0xffffffffc00fe000 -s .data 0xffffffffc00ff000 -s __bug_table 0xffffffffc00ff100 -s .rodata.str1.1 0xffffffffc00fe05c -s .rodata.str1.8 0xffffffffc00fe110" \

-ex "target remote localhost:1000"

资料来源:

0

0

vote

Article Rating

linux内核的论文,Linux kernel 初探相关推荐

  1. 【Linux 内核】编译 Linux 内核 ⑤ ( 查看 .config 编译配置文件 | 正式编译内核 )

    文章目录 一.查看 .config 编译配置文件 二.正式编译内核 一.查看 .config 编译配置文件 在上一篇博客 [Linux 内核]编译 Linux 内核 ④ ( 打开 Linux 内核编译 ...

  2. 【Linux 内核】编译 Linux 内核 ① ( 下载指定版本的 Linux 内核源码 | Linux 内核版本号含义 | 主版本号 | 次版本号 | 小版本号 | 稳定版本 )

    文章目录 一.下载 Linux 内核 1.下载最新版本 Linux 内核 2.下载指定版本 Linux 内核 二.Linux 内核版本号含义 一.下载 Linux 内核 1.下载最新版本 Linux ...

  3. 简述arm linux内核启动流程,Linux内核启动过程和Bootloader(总述)

    1.Linux内核启动过程概述 一个嵌入式 Linux 系统从软件角度看可以分为四个部分:引导加载程序(Bootloader),Linux 内核,文件系统,应用程序.其中 Bootloader是系统启 ...

  4. 武汉linux内核好找吗,Linux内核入门

    Linux内核入门 收藏 如何获取Linux内核源代码 下载Linux内核当然要去http://www.kernel.org/了,网站提供了两种文件下载,一种是完整的Linux内核,另一种是内核增量补 ...

  5. linux 内核优化实战,Linux编译优化必须掌握的几个姿势总结

    01.编译选项和内核编译 Linux内核(英语:linux kernel),是一种计算机操作系统内核,已C语言和汇编语言写成,匹配POSIX标准,以GNU通用公共许可证发布.从技术上说Linux只是一 ...

  6. 编译Linux内核没有zImage,Linux 编译系统的简单介绍与内核编译安装

    这里不只是讲怎样编译.安装Linux内核的,更主要的是介绍内核的编译系统和各个重要的文件.最后还利用学到的编译.安装Linux内核去修改Linux的01调度变成随机调度.如果你只是需要编译.安装内核的 ...

  7. Linux内核及主流Linux发行版对应关系汇总

    Linux内核及主流Linux发行版对应关系汇总 如需转载请标明出处:http://blog.csdn.net/itas109 QQ技术交流群:129518033 文章目录 Linux内核及主流Lin ...

  8. linux的头文件下载,Linux内核头文件(linux headers)

    更新 Linux 内核头文件(linux headers) 三 8th, 2013 2,474 views | 发表评论 | Trackback 一般来说,如果不是自己编译 kernel,那么更新头文 ...

  9. 【Linux 内核】编译 Linux 内核 ⑦ ( 安装内核模块 | 安装内核 | 重启系统 | 查看当前内核版本 )

    文章目录 一.安装内核模块 二.安装内核 三.重启系统 四.查看当前内核版本 一.安装内核模块 确保 Linux 内核编译完成 , 没有任何报错之后 ; 参考 [Linux 内核]编译 Linux 内 ...

最新文章

  1. LeetCode(9.回文数)JAVA
  2. Reflector for .NET
  3. java 反射类成员_java 反射(二)类成员
  4. Enterprise Vault 10.0.4 FOR Exchange2013 部署之三-日记邮件归档
  5. 马云又出金句:文凭只是学费的收据,真正的文凭是生活中奋斗来的
  6. 面试准备每日五题:C++(五)——野指针、句柄指针、newdeletemallocfree、extern、类成员权限
  7. 工作中最重要的个人品质--独立
  8. 8000401a错误解决方案(Office Word,Excel,PowerPoit应用程序无法访问)
  9. 大学生网页作业成品——基于HTML网上书城项目的设计与实现
  10. mysql catalog def_在ArcCatalog里如何连接数据库-百度经验
  11. 联想小新一键恢复小孔_联想一键恢复系统怎么用?小新Air 13 Pro怎么还原操作系统?...
  12. cocos2d-x项目101次相遇:使用触摸事件移动 精灵
  13. sklearn多分类任务自定义cv交叉验证scoring
  14. 分析ajax爬取果壳网
  15. Android tint颜色渲染
  16. 为何大数据会扼杀企业
  17. pivot和unpivot函数
  18. 业务:财务会计业务知识
  19. 2021桂林ccpc B. A Plus B Problem,set维护
  20. 如何通过短信网关发送Wappush消息?

热门文章

  1. idea常用的快捷键(黑马培训班)
  2. STM32时钟体系结构
  3. 创建私有CA并进行证书申请。
  4. Mongodb数据库命令端常用操作
  5. AfterEffect插件-常规功能开发-改变渲染输出位置-js脚本开发-AE插件
  6. JMeter学习自动化badboy脚本开发技术
  7. 1434 Best Cow Fences
  8. python图片定位_[求助贴]python图片全屏截取+定位坐标+图片识别
  9. POJ1416 Shredding Company ACM解题报告(DFS回溯+剪枝)
  10. “道路”转向“街道”的活力街区建设