成都新公园,418万平方米的公园

该日志由 samool 发表于 2020-12-07 19:53:32

初次对成都加深印象是在什么时候呢?应该是那句歌词唱的那样,走在成都的街道上也停不下来,这首歌火遍了校园,到处都能听到哼着小曲的人。

成都的这个热闹的大城市,一个又一个远方的游客的吸引着人们,成都的发展观光业也在这里盛行,父亲总是一种令人羡慕的语气,他的旧同学谁又说成都旅行了,这样,这里真的是一个好地方,

这个公园的名字叫天府艺术公园,地点在成都老西门,这个公园的建成范围有多大?

这个公园的目的是再现“蜀风揽胜画卷”,既然是艺术类的,那一定展示了艺术性的内容,这个公园还引进了展馆。

有天府艺术馆、天府美术馆、天府图书馆,下面详细说明这些展馆。

天府美术馆,这个展馆富有艺术性,屋顶是用芙蓉花瓣形状的金属素材建造的,从远处看,这个美术馆是盛开在湖里的芙蓉花。

天府艺术馆和图书馆的屋顶造型是斜向上的斜面曲面,对面就是美术馆,今后,如果小队友们如果有读书的欲望,在图书馆中读书,读着书,同时可以欣赏美丽的设计,精神的喜悦心情,同时也能好转,这是什么人的人梦想的生活!

该公园的建设是一个文化艺术爱好者争相,但他们在图书馆里徜徉中,美术馆也培养情操,比他们两人呐喊,也在一个地方停留的时间长了,特别是也改变了,从任何角度而言的一种心情,这是文艺爱好者的理想的天堂,今后,这里很有可能成为成都的“新地标”,对这里感兴趣的朋友们不要着急,这个公园将在2021年8月之前亮相。

该日志标签: 成都, 朋友, 同学, 心情, 生活, 城市, 文化, 内容, 设计, 歌词, 公园, 素材, 父亲, 旅行, 时间, 文艺, 时候, 天府, 美术馆, 图书馆

Golang 汇编入门知识总结

该日志由 samool 发表于 2020-09-27 14:35:04

作者:ivansli,腾讯 IEG 运营开发工程师

在深入学习 Golang 的 runtime 和标准库实现的时候发现,如果对 Golang 汇编没有一定了解的话,很难深入了解其底层实现机制。在这里整理总结了一份基础的 Golang 汇编入门知识,通过学习之后能够对其底层实现有一定的认识。

0. 为什么写本文

平时业务中一直使用 PHP 编写代码,但是一直对 Golang 比较感兴趣,闲暇、周末之余会看一些 Go 底层源码。

近日在分析 go 的某些特性底层功能实现时发现:有些又跟 runtime 运行时有关,而要掌握这一部分的话,有一道坎是绕不过去的,那就是 Go 汇编。索性就查阅了很多大佬们写的资料,在阅读之余整理总结了一下,并在这里分享给大家。

本文使用 Go 版本为 go1.14.1

1. 为什么需要汇编

众所周知,在计算机的世界里,只有 2 种类型。那就是:0 和 1。

计算机工作是由一系列的机器指令进行驱动的,这些指令又是一组二进制数字,其对应计算机的高低电平。而这些机器指令的集合就是机器语言,这些机器语言在最底层是与硬件一一对应的。

显而易见,这样的机器指令有一个致命的缺点:可阅读性太差(恐怕也只有天才和疯子才有能力把控得了)。

为了解决可读性的问题以及代码编辑的需求,于是就诞生了最接近机器的语言:汇编语言(在我看来,汇编语言更像一种助记符,这些人们容易记住的每一条助记符都映射着一条不容易记住的由 0、1 组成的机器指令。你觉得像不像域名与 IP 地址的关系呢?)。

1.1 程序的编译过程

以 C 语言为例来说,从 hello.c 的源码文件到 hello 可执行文件,经过编译器处理,大致分为几个阶段:

编译器在不同的阶段会做不同的事情,但是有一步是可以确定的,那就是:源码会被编译成汇编,最后才是二进制。

2. 程序与进程

源码经过编译之后,得到一个二进制的可执行 文件文件这两个字也就表明,目前得到的这个文件跟其他文件对比,除了是具有一定的格式(Linux 中是 ELF 格式,即:可运行可链接。executable linkable formate)的二进制组成,并没什么区别。

在 Linux 中文件类型大致分为 7 种:

b: 块设备文件c:字符设备文件d:目录-:普通文件l:链接s:socketp:管道

通过上面可以看到,可执行文件 main 与源码文件 main.go,都是同一种类型,属于普通文件。(当然了,在 Unix 中有一句很经典的话:一切皆文件)。

那么,问题来了:

  1. 什么是程序?
  2. 什么是进程?

2.1 程序

维基百科告诉我们:程序是指一组指示计算机或其他具有消息处理能力设备每一步动作的指令,通常用某种程序设计语言编写,运行于某种目标体系结构上。

从某个层面来看,可以把程序分为静态程序、动态程序:静态程序:单纯的指具有一定格式的可执行二进制文件。动态程序:则是静态可执行程序文件被加载到内存之后的一种运行时模型(又称为进程)。

2.2 进程

首先,要知道的是,进程是分配系统资源的最小单位,线程(带有时间片的函数)是系统调度的最小单位。进程包含线程,线程所属于进程。

创建进程一般使用 fork 方法(通常会有个拉起程序,先 fork 自身生成一个子进程。然后,在该子进程中通过 exec 函数把对应程序加载进来,进而启动目标进程。当然,实际上要复杂得多),而创建线程则是使用 pthread 线程库。

以 32 位 Linux 操作系统为例,进程经典的虚拟内存结构模型如下图所示:

其中,有两处结构是静态程序所不具有的,那就是 运行时堆(heap)运行时栈(stack)

运行时堆从低地址向高地址增长,申请的内存空间需要程序员自己或者由 GC 释放。运行时栈从高地址向低地址增长,内存空间在当前栈桢调用结束之后自动释放(并不是清除其所占用内存中数据,而是通过栈顶指针 SP 的移动,来标识哪些内存是正在使用的)。

3. Go 汇编

对于 Go 编译器而言,其输出的结果是一种抽象可移植的汇编代码,这种汇编(Go 的汇编是基于 Plan9 的汇编)并不对应某种真实的硬件架构。Go 的汇编器会使用这种伪汇编,再为目标硬件生成具体的机器指令。

伪汇编这一个额外层可以带来很多好处,最主要的一点是方便将 Go 移植到新的架构上。

相关的信息可以参考 Rob PikeThe Design of the Go Assembler

要了解 Go 的汇编器最重要的是要知道 Go 的汇编器不是对底层机器的直接表示,即 Go 的汇编器没有直接使用目标机器的汇编指令。Go 汇编器所用的指令,一部分与目标机器的指令一一对应,而另外一部分则不是。这是因为编译器套件不需要汇编器直接参与常规的编译过程。

相反,编译器使用了一种半抽象的指令集,并且部分指令是在代码生成后才被选择的。汇编器基于这种半抽象的形式工作,所以虽然你看到的是一条 MOV 指令,但是工具链针对对这条指令实际生成可能完全不是一个移动指令,也许会是清除或者加载。也有可能精确的对应目标平台上同名的指令。概括来说,特定于机器的指令会以他们的本尊出现, 然而对于一些通用的操作,如内存的移动以及子程序的调用以及返回通常都做了抽象。细节因架构不同而不一样,我们对这样的不精确性表示歉意,情况并不明确。

汇编器程序的工作是对这样半抽象指令集进行解析并将其转变为可以输入到链接器的指令。

The most important thing to know about Go’s assembler is that it is not a direct representation of the underlying machine. Some of the details map precisely to the machine, but some do not. This is because the compiler suite needs no assembler pass in the usual pipeline. Instead, the compiler operates on a kind of semi-abstract instruction set, and instruction selection occurs partly after code generation. The assembler works on the semi-abstract form, so when you see an instruction like MOV what the toolchain actually generates for that operation might not be a move instruction at all, perhaps a clear or load.

Or it might correspond exactly to the machine instruction with that name. In general, machine-specific operations tend to appear as themselves, while more general concepts like memory move and subroutine call and return are more abstract. The details vary with architecture, and we apologize for the imprecision; the situation is not well-defined.

The assembler program is a way to parse a description of that semi-abstract instruction set and turn it into instructions to be input to the linker.

Go 汇编使用的是 caller-save模式,被调用函数的入参参数、返回值都由调用者维护、准备。因此,当需要调用一个函数时,需要先将这些工作准备好,才调用下一个函数,另外这些都需要进行内存对齐,对齐的大小是 sizeof(uintptr)。

3.1 几个概念

在深入了解 Go 汇编之前,需要知道的几个概念:

  • 栈:进程、线程、goroutine 都有自己的调用栈,先进后出(FILO)
  • 栈帧:可以理解是函数调用时,在栈上为函数所分配的内存区域
  • 调用者:caller,比如:A 函数调用了 B 函数,那么 A 就是调用者
  • 被调者:callee,比如:A 函数调用了 B 函数,那么 B 就是被调者

3.2 Go 的核心寄存器

go 汇编中有 4 个核心的伪寄存器,这 4 个寄存器是编译器用来维护上下文、特殊标识等作用的:

寄存器说明
SB(Static base pointer)global symbols
FP(Frame pointer)arguments and locals
PC(Program counter)jumps and branches
SP(Stack pointer)top of stack
  • FP: 使用如symbol+offset(FP)的方式,引用 callee 函数的入参参数。例如arg0+0(FP),arg1+8(FP),使用 FP 必须加 symbol ,否则无法通过编译(从汇编层面来看,symbol 没有什么用,加 symbol 主要是为了提升代码可读性)。另外,需要注意的是:往往在编写 go 汇编代码时,要站在 callee 的角度来看(FP),在 callee 看来,(FP)指向的是 caller 调用 callee 时传递的第一个参数的位置。假如当前的 callee 函数是 add,在 add 的代码中引用 FP,该 FP 指向的位置不在 callee 的 stack frame 之内。而是在 caller 的 stack frame 上,指向调用 add 函数时传递的第一个参数的位置,经常在 callee 中用symbol+offset(FP)来获取入参的参数值。
  • SB: 全局静态基指针,一般用在声明函数、全局变量中。
  • SP: 该寄存器也是最具有迷惑性的寄存器,因为会有伪 SP 寄存器和硬件 SP 寄存器之分。plan9 的这个伪 SP 寄存器指向当前栈帧第一个局部变量的结束位置(为什么说是结束位置,可以看下面寄存器内存布局图),使用形如 symbol+offset(SP) 的方式,引用函数的局部变量。offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间。假如局部变量都是 8 字节,那么第一个局部变量就可以用 localvar0-8(SP) 来表示。与硬件寄存器 SP 是两个不同的东西,在栈帧 size 为 0 的情况下,伪寄存器 SP 和硬件寄存器 SP 指向同一位置。手写汇编代码时,如果是 symbol+offset(SP)形式,则表示伪寄存器 SP。如果是 offset(SP)则表示硬件寄存器 SP。务必注意:对于编译输出(go tool compile -S / go tool objdump)的代码来讲,所有的 SP 都是硬件 SP 寄存器,无论是否带 symbol(这一点非常具有迷惑性,需要慢慢理解。往往在分析编译输出的汇编时,看到的就是硬件 SP 寄存器)。
  • PC: 实际上就是在体系结构的知识中常见的 pc 寄存器,在 x86 平台下对应 ip 寄存器,amd64 上则是 rip。除了个别跳转之外,手写 plan9 汇编代码时,很少用到 PC 寄存器。

通过上面的讲解,想必已经对 4 个核心寄存器的区别有了一定的认识(或者是更加的迷惑、一头雾水)。那么,需要留意的是:如果是在分析编译输出的汇编代码时,要重点看 SP、SB 寄存器(FP 寄存器在这里是看不到的)。如果是,在手写汇编代码,那么要重点看 FP、SP 寄存器。

3.2.1 伪寄存器的内存模型

下图描述了栈桢与各个寄存器的内存关系模型,值得注意的是要站在 callee 的角度来看。

有一点需要注意的是,return addr 也是在 caller 的栈上的,不过往栈上插 return addr 的过程是由 CALL 指令完成的(在分析汇编时,是看不到关于 addr 相关空间信息的。在分配栈空间时,addr 所占用空间大小不包含在栈帧大小内)。

在 AMD64 环境,伪 PC 寄存器其实是 IP 指令计数器寄存器的别名。伪 FP 寄存器对应的是 caller 函数的帧指针,一般用来访问 callee 函数的入参参数和返回值。伪 SP 栈指针对应的是当前 callee 函数栈帧的底部(不包括参数和返回值部分),一般用于定位局部变量。伪 SP 是一个比较特殊的寄存器,因为还存在一个同名的 SP 真寄存器,真 SP 寄存器对应的是栈的顶部。

在编写 Go 汇编时,当需要区分伪寄存器和真寄存器的时候只需要记住一点:伪寄存器一般需要一个标识符和偏移量为前缀,如果没有标识符前缀则是真寄存器。比如(SP)、+8(SP)没有标识符前缀为真 SP 寄存器,而 a(SP)、b+8(SP)有标识符为前缀表示伪寄存器。

3.2.2 几点说明

我们这里对容易混淆的几点简单进行说明:

  • 伪 SP 和硬件 SP 不是一回事,在手写汇编代码时,伪 SP 和硬件 SP 的区分方法是看该 SP 前是否有 symbol。如果有 symbol,那么即为伪寄存器,如果没有,那么说明是硬件 SP 寄存器。
  • 伪 SP 和 FP 的相对位置是会变的,所以不应该尝试用伪 SP 寄存器去找那些用 FP+offset 来引用的值,例如函数的入参和返回值。
  • 官方文档中说的伪 SP 指向 stack 的 top,可能是有问题的。其指向的局部变量位置实际上是整个栈的栈底(除 caller BP 之外),所以说 bottom 更合适一些。
  • 在 go tool objdump/go tool compile -S 输出的代码中,是没有伪 SP 和 FP 寄存器的,我们上面说的区分伪 SP 和硬件 SP 寄存器的方法,对于上述两个命令的输出结果是没法使用的。在编译和反汇编的结果中,只有真实的 SP 寄存器。
3.2.3 IA64 和 plan9 的对应关系

在 plan9 汇编里还可以直接使用的 amd64 的通用寄存器,应用代码层面会用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15 这些寄存器,虽然 rbp 和 rsp 也可以用,不过 bp 和 sp 会被用来管理栈顶和栈底,最好不要拿来进行运算。

plan9 中使用寄存器不需要带 r 或 e 的前缀,例如 rax,只要写 AX 即可: MOVQ $101, AX = mov rax, 101

下面是通用通用寄存器的名字在 IA64 和 plan9 中的对应关系:

3.3 常用操作指令

下面列出了常用的几个汇编指令(指令后缀 Q 说明是 64 位上的汇编指令)

助记符指令种类用途示例
MOVQ传送数据传送MOVQ 48, AX // 把 48 传送到 AX
LEAQ传送地址传送LEAQ AX, BX // 把 AX 有效地址传送到 BX
PUSHQ传送栈压入PUSHQ AX // 将 AX 内容送入栈顶位置
POPQ传送栈弹出POPQ AX // 弹出栈顶数据后修改栈顶指针
ADDQ运算相加并赋值ADDQ BX, AX // 等价于 AX+=BX
SUBQ运算相减并赋值SUBQ BX, AX // 等价于 AX-=BX
CMPQ运算比较大小CMPQ SI CX // 比较 SI 和 CX 的大小
CALL转移调用函数CALL runtime.printnl(SB) // 发起调用
JMP转移无条件转移指令JMP 0x0185 //无条件转至 0x0185 地址处
JLS转移条件转移指令JLS 0x0185 //左边小于右边,则跳到 0x0185

4. 汇编分析

说了那么多,it is code show time。

4.1 如何输出 Go 汇编

对于写好的 go 源码,生成对应的 Go 汇编,大概有下面几种

  • 方法 1 先使用go build -gcflags "-N -l" main.go 生成对应的可执行二进制文件 再使用go tool objdump -s "main\." main 反编译获取对应的汇编

反编译时 "main\." 表示只输出 main 包中相关的汇编 "main\.main" 则表示只输出 main 包中 main 方法相关的汇编

  • 方法 2 使用go tool compile -S -N -l main.go 这种方式直接输出汇编
  • 方法 3 使用go build -gcflags="-N -l -S" main.go 直接输出汇编

注意:在使用这些命令时,加上对应的 flag,否则某些逻辑会被编译器优化掉,而看不到对应完整的汇编代码

-l 禁止内联 -N 编译时,禁止优化 -S 输出汇编代码

4.2 Go 汇编示例

go 示例代码

package main func add(a, b int) int{        sum := 0 // 不设置该局部变量sum,add栈空间大小会是0        sum = a+b        return sum} func main(){        println(add(1,2))}

编译 go 源代码,输出汇编

go tool compile -N -l -S main.go

截取主要汇编如下:

"".add STEXT nosplit size=60 args=0x18 locals=0x10        0x0000 00000 (main.go:3)        TEXT    "".add(SB), NOSPLIT, $16-24        0x0000 00000 (main.go:3)        SUBQ    $16, SP  ;;生成add栈空间        0x0004 00004 (main.go:3)        MOVQ    BP, 8(SP)        0x0009 00009 (main.go:3)        LEAQ    8(SP), BP    ;; ...omitted FUNCDATA stuff...        0x000e 00014 (main.go:3)        MOVQ    $0, "".~r2+40(SP) ;;初始化返回值        0x0017 00023 (main.go:4)        MOVQ    $0, "".sum(SP) ;;局部变量sum赋为0        0x001f 00031 (main.go:5)        MOVQ    "".a+24(SP), AX  ;;取参数a        0x0024 00036 (main.go:5)        ADDQ    "".b+32(SP), AX ;;等价于AX=a+b        0x0029 00041 (main.go:5)        MOVQ    AX, "".sum(SP)  ;;赋值局部变量sum        0x002d 00045 (main.go:6)        MOVQ    AX, "".~r2+40(SP) ;;设置返回值        0x0032 00050 (main.go:6)        MOVQ    8(SP), BP        0x0037 00055 (main.go:6)        ADDQ    $16, SP ;;清除add栈空间        0x003b 00059 (main.go:6)        RET    ...... "".main STEXT size=107 args=0x0 locals=0x28        0x0000 00000 (main.go:9)        TEXT    "".main(SB), $40-0    ......        0x000f 00015 (main.go:9)        SUBQ    $40, SP ;; 生成main栈空间        0x0013 00019 (main.go:9)        MOVQ    BP, 32(SP)        0x0018 00024 (main.go:9)        LEAQ    32(SP), BP    ;; ...omitted FUNCDATA stuff...        0x001d 00029 (main.go:10)       MOVQ    $1, (SP) ;;add入参:1        0x0025 00037 (main.go:10)       MOVQ    $2, 8(SP) ;;add入参:2        0x002e 00046 (main.go:10)       CALL    "".add(SB) ;;调用add函数        0x0033 00051 (main.go:10)       MOVQ    16(SP), AX        0x0038 00056 (main.go:10)       MOVQ    AX, ""..autotmp_0+24(SP)        0x003d 00061 (main.go:10)       CALL    runtime.printlock(SB)        0x0042 00066 (main.go:10)       MOVQ    ""..autotmp_0+24(SP), AX        0x0047 00071 (main.go:10)       MOVQ    AX, (SP)        0x004b 00075 (main.go:10)       CALL    runtime.printint(SB)        0x0050 00080 (main.go:10)       CALL    runtime.printnl(SB)        0x0055 00085 (main.go:10)       CALL    runtime.printunlock(SB)        0x005a 00090 (main.go:11)       MOVQ    32(SP), BP        0x005f 00095 (main.go:11)       ADDQ    $40, SP ;;清除main栈空间        0x0063 00099 (main.go:11)       RET    ......

这里列举了一个简单的 int 类型 加法示例,实际开发中会遇到各种参数类型,要复杂的多,这里只是抛砖引玉 :)

4.3 Go 汇编解析

针对 4.2 输出汇编,对重要核心代码进行分析。

4.3.1 add 函数汇编解析
  • TEXT "".add(SB), NOSPLIT|ABIInternal, $16-24

TEXT "".add TEXT 指令声明了 "".add 是 .text 代码段的一部分,并表明跟在这个声明后的是函数的函数体。在链接期,""这个空字符会被替换为当前的包名: 也就是说,"".add 在链接到二进制文件后会变成 main.add

(SB) SB 是一个虚拟的伪寄存器,保存静态基地址(static-base) 指针,即我们程序地址空间的开始地址。"".add(SB) 表明我们的符号位于某个固定的相对地址空间起始处的偏移位置 (最终是由链接器计算得到的)。换句话来讲,它有一个直接的绝对地址: 是一个全局的函数符号。

NOSPLIT: 向编译器表明不应该插入 stack-split 的用来检查栈需要扩张的前导指令。在我们 add 函数的这种情况下,编译器自己帮我们插入了这个标记: 它足够聪明地意识到,由于 add 没有任何局部变量且没有它自己的栈帧,所以一定不会超出当前的栈。不然,每次调用函数时,在这里执行栈检查就是完全浪费 CPU 时间了。

$0-16

24 指定了调用方传入的参数+返回值大小(24 字节=入参 a、b 大小 8字节*2+ 返回值8字节)> 通常来讲,帧大小后一般都跟随着一个参数大小,用减号分隔。(这不是一个减法操作,只是一种特殊的语法) 帧大小 $24-8 意味着这个函数有 24 个字节的帧以及 8 个字节的参数,位于调用者的帧上。如果 NOSPLIT 没有在 TEXT 中指定,则必须提供参数大小。对于 Go 原型的汇编函数,go vet 会检查参数大小是否正确。

In the general case, the frame size is followed by an argument size, separated by a minus sign. (It’s not a subtraction, just idiosyncratic syntax.) The frame size $24-8 states that the function has a 24-byte frame and is called with 8 bytes of argument, which live on the caller’s frame. If NOSPLIT is not specified for the TEXT, the argument size must be provided. For assembly functions with Go prototypes, go vet will check that the argument size is correct.
  • SUBQ $16, SPSP 为栈顶指针,该语句等价于 SP-=16(由于栈空间是向下增长的,所以开辟栈空间时为减操作),表示生成 16 字节大小的栈空间。
  • MOVQ $0, "".~r2+40(SP)此时的 SP 为 add 函数栈的栈顶指针,40(SP)的位置则是 add 返回值的位置,该位置位于 main 函数栈空间内。该语句设置返回值类型的 0 值,即初始化返回值,防止得到脏数据(返回值类型为 int,int 的 0 值为 0)。
  • MOVQ "".a+24(SP), AX从 main 函数栈空间获取入参 a 的值,存到寄存器 AX
  • ADDQ "".b+32(SP), AX从 main 函数栈空间获取入参 b 的值,与寄存器 AX 中存储的 a 值相加,结果存到 AX。相当于 AX=a+b
  • MOVQ AX, "".~r2+40(SP)把 a+b 的结果放到 main 函数栈中, add(a+b)返回值所在的位置
  • ADDQ $16, SP归还 add 函数占用的栈空间
4.3.2 函数栈桢结构模型

根据 4.2 对应汇编绘制的函数栈桢结构模型

还记得前面提到的,Go 汇编使用的是 caller-save模式,被调用函数的参数、返回值、栈位置都需要由调用者维护、准备吗?

在函数栈桢结构中可以看到,add()函数的入参以及返回值都由调用者 main()函数维护。也正是因为如此,GO 有了其他语言不具有的,支持多个返回值的特性。

4.4 Go 汇编语法

这里重点讲一下函数声明、变量声明。

4.4.1 函数声明

来看一个典型的 Go 汇编函数定义

// func add(a, b int) int// 该add函数声明定义在同一个 package name 下的任意 .go文件中// 只有函数头,没有实现 // add函数的Go汇编实现// pkgname 默认是  ""TEXT pkgname·add(SB), NOSPLIT, $16-24    MOVQ a+0(FP), AX    ADDQ b+8(FP), AX    MOVQ AX, ret+16(FP)    RET

Go 汇编实现为什么是 TEXT 开头?仔细观察上面的进程内存布局图就会发现,我们的代码在是存储在.text 段中的,这里也就是一种约定俗成的起名方式。实际上在 plan9 中 TEXT 是一个指令,用来定义一个函数。

定义中的 pkgname 是可以省略的,(非想写也可以写上,不过写上 pkgname 的话,在重命名 package 之后还需要改代码,默认为 "") 编译器会在链接期自动加上所属的包名称。

中点 · 比较特殊,是一个 unicode 的中点,该点在 mac 下的输入方法是 option+shift+9。在程序被链接之后,所有的中点 ·都会被替换为句号 .,比如你的方法是 runtime·main,在编译之后的程序里的符号则是 runtime.main

简单总结一下, Go 汇编实现函数声明,格式为:

 静态基地址(static-base) 指针    |                  |         add函数入参+返回值总大小                  |               |TEXT pkgname·add(SB),NOSPLIT,$16-24      |      |                |函数所属包名  函数名          add函数栈帧大小
  • 函数栈帧大小:局部变量+可能需要的额外调用函数的参数空间的总大小,不包括调用其它函数时的 ret address 的大小。
  • (SB): SB 是一个虚拟寄存器,保存了静态基地址(static-base) 指针,即我们程序地址空间的开始地址。"".add(SB) 表明我们的符号位于某个固定的相对地址空间起始处的偏移位置 (最终是由链接器计算得到的)。换句话来讲,它有一个直接的绝对地址: 是一个全局的函数符号。
  • NOSPLIT: 向编译器表明,不应该插入 stack-split 的用来检查栈需要扩张的前导指令。在我们 add 函数的这种情况下,编译器自己帮我们插入了这个标记: 它足够聪明地意识到,add 不会超出当前的栈,因此没必要调用函数时在这里执行栈检查。
4.4.2 变量声明

汇编里的全局变量,一般是存储在 .rodata或者 .data段中。对应到 Go 代码,就是已初始化过的全局的 const、var 变量/常量。

使用 DATA 结合 GLOBL 来定义一个变量。

DATA 的用法为:

DATA symbol+offset(SB)/width, value

大多数参数都是字面意思,不过这个 offset 需要注意:其含义是该值相对于符号 symbol 的偏移,而不是相对于全局某个地址的偏移。

GLOBL 汇编指令用于定义名为 symbol 的全局变量,变量对应的内存宽度为 width,内存宽度部分必须用常量初始化。

GLOBL ·symbol(SB), width

下面是定义了多个变量的例子:

DATA ·age+0(SB)/4, $8  ;; 数值8为 4字节GLOBL ·age(SB), RODATA, $4 DATA ·pi+0(SB)/8, $3.1415926 ;; 数值3.1415926为float64, 8字节GLOBL ·pi(SB), RODATA, $8 DATA ·year+0(SB)/4, $2020 ;; 数值2020为 4字节GLOBL ·year(SB), RODATA, $4  ;; 变量hello 使用2个DATA来定义DATA ·hello+0(SB)/8, $"hello my" ;; `hello my` 共8个字节DATA ·hello+8(SB)/8, $"   world" ;; `   world` 共8个字节(3个空格)GLOBL ·hello(SB), RODATA, $16 ;; `hello my   world`  共16个字节  DATA ·hello<>+0(SB)/8, $"hello my" ;; `hello my` 共8个字节DATA ·hello<>+8(SB)/8, $"   world" ;; `   world` 共8个字节(3个空格)GLOBL ·hello<>(SB), RODATA, $16 ;; `hello my   world`  共16个字节

大部分都比较好理解,不过这里引入了新的标记 <>,这个跟在符号名之后,表示该全局变量只在当前文件中生效,类似于 C 语言中的 static。如果在另外文件中引用该变量的话,会报 relocation target not found 的错误。

5. 手写汇编实现功能

在 Go 源码中会看到一些汇编写的代码,这些代码跟其他 go 代码一起组成了整个 go 的底层功能实现。下面,我们通过一个简单的 Go 汇编代码示例来实现两数相加功能。

5.1 使用 Go 汇编实现 add 函数

Go 代码

package main func add(a, b int64) int64 func main(){        println(add(2,3))}

Go 源码中 add()函数只有函数签名,没有具体的实现(使用 GO 汇编实现)

使用 Go 汇编实现的 add()函数

TEXT ·add(SB), $0-24 ;; add栈空间为0,入参+返回值大小=24字节        MOVQ x+0(FP), AX ;; 从main中取参数:2        ADDQ y+8(FP), AX ;; 从main中取参数:3         MOVQ AX, ret+16(FP) ;; 保存结果到返回值         RET

把 Go 源码与 Go 汇编编译到一起(我这里,这两个文件在同一个目录)

go build -gcflags "-N -l" .

我这里目录为 demo1,所以得到可执行程序 demo1,运行得到结果:5

5.2 反编译可执行程序

对 5.1 中得到的可执行程序 demo1 使用 objdump 进行反编译,获取汇编代码

go tool objdump -s "main\." demo1

得到汇编

......TEXT main.main(SB) /root/go/src/demo1/main.go  main.go:5   0x4581d0     64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX  main.go:5   0x4581d9     483b6110                CMPQ 0x10(CX), SP  main.go:5   0x4581dd     7655                    JBE 0x458234  main.go:5   0x4581df     4883ec28                SUBQ $0x28, SP ;;生成main栈桢  main.go:5   0x4581e3     48896c2420              MOVQ BP, 0x20(SP)  main.go:5   0x4581e8     488d6c2420              LEAQ 0x20(SP), BP  main.go:6   0x4581ed     48c7042402000000        MOVQ $0x2, 0(SP) ;;参数值 2  main.go:6   0x4581f5     48c744240803000000      MOVQ $0x3, 0x8(SP) ;;参数值 3  main.go:6   0x4581fe     e83d000000              CALL main.add(SB);;call add  main.go:6   0x458203     488b442410              MOVQ 0x10(SP), AX  main.go:6   0x458208     4889442418              MOVQ AX, 0x18(SP)  main.go:6   0x45820d     e8fe2dfdff              CALL runtime.printlock(SB)  main.go:6   0x458212     488b442418              MOVQ 0x18(SP), AX  main.go:6   0x458217     48890424                MOVQ AX, 0(SP)  main.go:6   0x45821b     e87035fdff              CALL runtime.printint(SB)  main.go:6   0x458220     e87b30fdff              CALL runtime.printnl(SB)  main.go:6   0x458225     e8662efdff              CALL runtime.printunlock(SB)  main.go:7   0x45822a     488b6c2420              MOVQ 0x20(SP), BP  main.go:7   0x45822f     4883c428                ADDQ $0x28, SP  main.go:7   0x458233     c3                      RET  main.go:5   0x458234     e89797ffff              CALL runtime.morestack_noctxt(SB)  main.go:5   0x458239     eb95                    JMP main.main(SB) ;; 反编译得到的汇编与add_amd64.s文件中的汇编大致操作一致TEXT main.add(SB) /root/go/src/demo1/add_amd64.s  add_amd64.s:2   0x458240    488b442408    MOVQ 0x8(SP), AX ;; 获取第一个参数  add_amd64.s:3   0x458245    4803442410    ADDQ 0x10(SP), AX ;;参数a+参数b  add_amd64.s:5   0x45824a    4889442418    MOVQ AX, 0x18(SP) ;;保存计算结果  add_amd64.s:7   0x45824f    c3            RET

通过上面操作,可知:

  1. (FP)伪寄存器,只有在编写 Go 汇编代码时使用。FP 伪寄存器指向 caller 传递给 callee 的第一个参数
  2. 使用 go tool compile / go tool objdump 得到的汇编中看不到(FP)寄存器的踪影

6. Go 调试工具

这里推荐 2 个 Go 代码调试工具。

6.1 gdb 调试 Go 代码

测试代码

package main type Ier interface{        add(a, b int) int        sub(a, b int) int} type data struct{        a, b int} func (*data) add(a, b int) int{        return a+b} func (*data) sub(a, b int) int{        return a-b} func main(){        var t Ier = &data{3,4}         println(t.add(1,2))        println(t.sub(3,2))}

编译 go build -gcflags "-N -l" -o main

使用 GDB 调试

> gdb main GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-80.el7Copyright (C) 2013 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.htmlThis is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.  Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-redhat-linux-gnu".For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>...Reading symbols from /root/go/src/interface/main...done.Loading Go Runtime support.(gdb) list   // 显示源码14      func (*data) add(a, b int) int{15              return a+b16      }1718      func (*data) sub(a, b int) int{19              return a-b20      }212223      func main(){(gdb) list24              var t Ier = &data{3,4}2526              println(t.add(1,2))27              println(t.sub(3,2))28      }29(gdb) b 26  // 在源码26行处设置断点Breakpoint 1 at 0x45827c: file /root/go/src/interface/main.go, line 26.(gdb) rStarting program: /root/go/src/interface/main Breakpoint 1, main.main () at /root/go/src/interface/main.go:2626              println(t.add(1,2))(gdb) info locals  // 显示变量t = {tab = 0x487020 <data,main.Ier>, data = 0xc000096000}(gdb) ptype t  // 打印t的结构type = struct runtime.iface {    runtime.itab *tab;    void *data;}(gdb) p *t.tab.inter  // 打印t.tab.inter指针指向的数据$2 = {typ = {size = 16, ptrdata = 16, hash = 2491815843, tflag = 7 '\a', align = 8 '\b', fieldAlign = 8 '\b',    kind = 20 '\024', equal = {void (void *, void *, bool *)} 0x466ec0,    gcdata = 0x484351 "\002\003\004\005\006\a\b\t\n\f\r\016\017\020\022\025\026\030\033\034\036\037\"&(,-5<BUXx\216\231\330\335\377", str = 6568, ptrToThis = 23808}, pkgpath = {bytes = 0x4592b4 ""}, mhdr =  []runtime.imethod = {{name = 277,      ityp = 48608}, {name = 649, ityp = 48608}}}(gdb) disass  // 显示汇编Dump of assembler code for function main.main:   0x0000000000458210 <+0>:     mov    %fs:0xfffffffffffffff8,%rcx   0x0000000000458219 <+9>:     cmp    0x10(%rcx),%rsp   0x000000000045821d <+13>:    jbe    0x458324 <main.main+276>   0x0000000000458223 <+19>:    sub    $0x50,%rsp   0x0000000000458227 <+23>:    mov    %rbp,0x48(%rsp)   0x000000000045822c <+28>:    lea    0x48(%rsp),%rbp   0x0000000000458231 <+33>:    lea    0x10dc8(%rip),%rax        # 0x469000   0x0000000000458238 <+40>:    mov    %rax,(%rsp)   0x000000000045823c <+44>:    callq  0x40a5c0 <runtime.newobject>

常用的 gdb 调试命令

  • run
  • continue
  • break
  • backtrace 与 frame
  • info break、locals
  • list 命令
  • print 和 ptype 命令
  • disass

除了 gdb,另外推荐一款 gdb 的增强版调试工具 cgdb

https://cgdb.github.io/

效果如下图所示,分两个窗口:上面显示源代码,下面是具体的命令行调试界面(跟 gdb 一样):

#### 6.2 delve 调试代码

delve 项目地址

https://github.com/go-delve/delve

带图形化界面的 dlv 项目地址

https://github.com/aarzilli/gdlv

dlv 的安装使用,这里不再做过多讲解,感兴趣的可以尝试一下。

  • gdb 作为调试工具自是不用多说,比较老牌、强大,可以支持多种语言。
  • delve 则是使用 go 语言开发的,用来调试 go 的工具,功能也是十分强大,打印结果可以显示 gdb 支持不了的东西,这里不再做过多讲解,有兴趣的可以查阅相关资料。

7. 总结

对于 Go 汇编基础大致需要熟悉下面几个方面:

通过上面的例子相信已经让你对 Go 的汇编有了一定的理解。当然,对于大部分业务开发人员来说,只要看的懂即可。如果想进一步的了解,可以阅读相关的资料或者书籍。

最后想说的是:鉴于个人能力有限,在阅读过程中你可能会发现存在的一些问题或者缺陷,欢迎各位大佬指正。如果感兴趣的话,也可以一起私下交流。

8. 参考资料

在整理的过程中,部分参考、引用下面链接地址内容。有一些写的还是不错的,感兴趣的同学可以阅读。

[1] https://github.com/cch123/golang-notes/blob/master/assembly.md plan9 assembly

[2] https://segmentfault.com/a/1190000019753885 汇编入门

[3] https://www.davidwong.fr/goasm/ Go Assembly by Example

[4] https://juejin.im/post/6844904005630443533#heading-3

[5] https://github.com/go-internals-cn/go-internals/blob/master/chapter1_assembly_primer/README.md

[6] https://lrita.github.io/2017/12/12/golang-asm/

[7] https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-01-basic.html

该日志标签: sp, 字符, 函数, 消息, 进程, 动作, 腾讯, 工作, 目标, 移动, 目录, 汇编, 系统, 静态, 驱动, 经典, ip, 工具, 错误, 世界, 内容, live, 文件, it, 设计, 开发, 方法, 程序, 运行, 数字, 宽度, 链接, 输入, 代码, 资料, c, 信息, bug, 域名, 问题, 命令, map, 数据, 时间, 保存, 运算, 工程师, 程序员, 环境, file, int, 特性, 细节, 参数, 变量, 指令, unicode, 内存, root, type, 调试, input, o, 运营, 缺点, 时候, 语法, 文档, 语言, main, add, 寄存器

【Redis持久化】服务器突然挂了!Redis缓存都在内存中啊

该日志由 samool 发表于 2020-09-27 14:29:53

我是Redis,一个叫Antirez的男人把我带到了这个世界上。

“快醒醒!快醒醒!”,隐隐约约,我听到有人在叫我。

慢慢睁开眼睛,原来旁边是MySQL大哥。

“我怎么睡着了?”

“嗨,你刚才是不是出现了错误,整个进程都崩溃了!害得一大堆查询请求都给我怼过来了!”,MySQL说到。

刚刚醒来,脑子还有点懵,MySQL大哥扶我起来继续工作。

“糟了!我之前缓存的数据全都不见了!”

“WTF?你没有做持久化吗?”,MySQL大哥一听脸色都变了。

我尴尬的摇了摇头,“我都是保存在内存中的,所以才那么快啊”

“那也可以在硬盘上保存一下啊,遇到这种情况全部从头再来建立缓存,这不浪费时间嘛!”

我点了点头,“让我琢磨一下,看看怎么做这个持久化”。

RDB持久化

没几天,我就拿出了一套方案:RDB

既然我的数据都在内存中存放着,最简单的就是遍历一遍把它们全都写入文件中。

为了节约空间,我定义了一个二进制的格式,把数据一条一条码在一起,生成了一个RDB文件。

不过我的数据量有点大,要是全部备份一次得花不少时间,所以不能太频繁的去做这事,要不然我不用干正事了,光花时间去备份了。

还有啊,要是一直没有写入操作,都是读取操作,那我也不用重复备份,浪费时间。

思来想去,我决定提供一个配置参数,既可以支持周期性备份,也可以避免做无用功。

就像这样:

  • save 900 1 # 900秒(15分钟)内有1个写入
  • save 300 10 # 300秒(5分钟)内有10个写入
  • save 60 10000 # 60秒(1分钟)内有10000个写入

多个条件可以组合使用,只要上面一个条件满足,我就会去进行备份。

后来我又想了一下,这样还是不行,我得fork出一个子进程去做这件事,不能浪费我的时间。

有了备份文件,下次我再遇到崩溃退出,甚至服务器断电罢工了,只要我的备份文件还在,我就能在启动的时候读取,快速恢复之前的状态啦!

MySQL:binlog

我带着这套方案,兴冲冲的拿给了MySQL大哥看了,期待他给我一些鼓励。

“老弟,你这个方案有点问题啊”,没想到,他竟给我浇了一盆冷水。

“问题?有什么问题?”

“你看啊,你这个周期性去备份,周期还是分钟级别的,你可知道咱们这服务每秒钟都要响应多少请求,像你这样不得丢失多少数据?”,MySQL语重心长的说到。

我一下有些气短了,“可是,这个备份一次要遍历全部数据,开销还是挺大的,不适合高频执行啊”

“谁叫你一次遍历全部数据了?来来来,我给你看个东西”,MySQL大哥把我带到了一个文件目录下:

  • mysql-bin.000001
  • mysql-bin.000002
  • mysql-bin.000003
  • ···

“看,这些是我的二进制日志binlog,你猜猜看里面都装了些什么?”,MySQL大哥指着这一堆文件说到。

我看了一眼,全是一堆二进制数据,这哪看得懂,我摇了摇头。

“这里面呀记录了我对数据执行更改的所有操作,像是INSERTUPDATEDELETE等等动作,等我要进行数据恢复的时候就可以派上大用场了”

听他这么一说,我一下来了灵感!告别了MySQL大哥,回去研究起新的方案来了。

AOF持久化

你们也知道,我也是基于命令式的,每天的工作就是响应业务程序发来的命令请求。

回来以后,我决定照葫芦画瓢,学着MySQL大哥的样子,把我执行的所有写入命令都记录下来,专门写入了一个文件,并给这种持久化方式也取了一个名字:AOF(Append Only File)

不过我遇到了RDB方案同样的问题,我该多久写一次文件呢?

我肯定不能每执行一条写入命令就记录到文件中,那会严重拖垮我的性能!我决定准备一个缓冲区,然后把要记录的命令先临时保存在这里,然后再择机写入文件,我把这个临时缓冲区叫做aof_buf

说干就干,我试了一下,竟然发现数据没有写入到文件中去。多方打听才知道,原来操作系统也有个缓存区,我写的数据被他缓存起来了,没有给我写入到文件中去,这不是坑爹呢嘛!

看来,我写完了还得要去刷新一下,把数据真正给写下去,思来想去,我还是提供一个参数,让业务程序去设置什么时候刷新吧。

appendfsync参数,三个取值:

  • always: 每个事件周期都同步刷新一次
  • everysec: 每一秒都同步刷新一次
  • no: 我只管写,让操作系统自己决定什么时候真正写入吧

AOF重写

这一次我不像之前那么冲动,我决定先试运行一段时间再去告诉MySQL大哥,免得又被他戳到软肋。

试用了一段时间,各方面都运行良好,不过我发现随着时间的推移,我写的这个AOF备份文件越来越大,越来越大!不仅非常占硬盘空间,复制移动,加载分析都非常的麻烦耗时。

我得想个办法把文件给压缩一下,我把这个过程叫做AOF重写

一开始,我打算去分析原来的AOF文件,然后将其中的冗余指令去掉,来给AOF文件瘦瘦身,不过我很快放弃了这个想法,这工作量实在太大了,分析起来也颇为麻烦,浪费很多精力跟时间。

原来的一条条记录这种方式实在是太笨了,数据改来改去,有很多中间状态都没用,我何不就把最终都数据状态记录下来就好了?

比如:

  • RPUSH name_list'编程技术宇宙'
  • RPUSH name_list'帅地玩编程'
  • RPUSH name_list'后端技术学堂'

可以合并成一条搞定:

  • RPUSH name_list '编程技术宇宙' '帅地玩编程' '后端技术学堂'

AOF文件重写的思路我是有了,不过这件事干起来还是很耗时间,我决定和RDB方式一样,fork出一个子进程来做这件事情。

谨慎如我,发现这样做之后,子进程在重写期间,我要是修改了数据,就会出现和重写的内容不一致的情况!MySQL大哥肯定会挑刺儿,我还得把这个漏洞给补上。

于是,我在之前的aof_buf之外,又准备了一个缓冲区:AOF重写缓冲区

从创建重写子进程开始的那一刻起,我把后面来的写入命令也copy一份写到这个重写缓冲区中,等到子进程重写AOF文件结束之后,我再把这个缓冲区中的命令写入到新的AOF文件中。

最后再重命名新的AOF文件,替换掉原来的那个臃肿不堪的大文件,终于大功告成!

再三确定我的思路没有问题之后,我带着新的方案再次找到了MySQL大哥,我都做到这份儿上了,这一次,想必他应该无话可说了吧?

MySQL大哥看了我的方案露出了满意的笑容,只是问了一个问题:

这AOF方案这么好了,RDB方案是不是可以不要了呢?

万万没想到,他居然问我这个问题,我竟陷入了沉思,你觉得我该怎么回答好呢?

彩蛋

“你怎么又崩溃了?”

“不好意思,又遇到bug了,不过不用担心,我现在可以快速恢复了!”

“那老崩溃也不是事儿啊,你只有一个实例太不可靠了,去找几个帮手吧!”

该日志标签: 技术, 进程, 查询, 动作, 工作, 目录, 错误, 日志, 漏洞, 世界, 内容, 文件, 程序, 运行, 实例, 男人, mysql, 编程, bug, 周期, 问题, 命令, 同步, 数据, 记录, 时间, 参数, 指令, 内存, 崩溃, , 时候, aof, 写入, 重写

远程桌面连接报【由于CredSSP 加密数据库修正】错误解决办法

该日志由 samool 发表于 2020-09-16 14:48:04

今天连接远程服务器时,出现这个错误提示,其它服务器都是好好的,唯独这台新服务器就报错

image.png

错误原因:查看微软CredSSP更新日志

查看win10系统升级日志,果然找到了原因,是因为CVE-2018-0886 的 CredSSP 2018 年 5 月 8 日更新默认设置从“易受攻击”更改为“缓解”的更新。相关的 Microsoft 知识库编号已在 CVE-2018-0886 中列出。默认情况下,安装此更新后,修补的客户端无法与未修补的服务器进行通信。 使用本文中描述的互操作性矩阵和组策略设置来启用“允许的”配置。
具体的内容请自行阅读:https://support.microsoft.com/zh-cn/help/4093492/credssp-updates-for-cve-2018-0886-march-13-2018

解决办法1:修改策略组配置

win+R 打开运行运行,然后输入“gpedit.msc”打开 如下图所示,在左侧窗口依次找到策略路径:“计算机配置”->“管理模板”->“系统”->“凭据分配” 然后右侧窗口设置名称中找到: 加密数据库修正 项目,选择”已启用“,保护级别选”易受攻击“,确定即可解决。

image.png

解决办法2:修改注册表

这里建议先使用上面组策略的方式进行解决,如果解决不了,再使用注册表方式,因为注册表方式没有组策略这种图形化界面的简单方便。

win+R 打开运行菜单,然后输入“regedit” 按Enter键即可打开如下的注册表,按照图示,找到此路径“
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters”

1377250-20180718093113200-592759675.png

然后在右侧窗口双击“AllowEncryptionOracle” 并把值设置成“2” (跟上面组策略设置效果一样)然后确定。如下图所示
1377250-20180718093233576-872877050.png

0表示强制更新的客户端
1表示缓解
2表示易受攻击 (跟上面组策略设置效果一样,有木有)

总结

今天主要是介绍下win10 mstsc远程遇到的坑“这可能是由于CredSSP 加密数据库修正”的两种解决方法。从微软官方更新日志入手,然后引出组策略以及注册表的解决方法。

图文也更容易让新手朋友也能按照步骤进行解决!

最后还是提醒下,新手朋友最好通过组策略的方式进行解决,因为微软也给出了警告: 如果使用注册表编辑器或其他方法修改注册表不当,可能会出现严重问题。

这些问题可能需要您重新安装操作系统。 Microsoft 不能保证可解决这些问题。 请自行承担修改注册表的风险。

部分图文来源:https://blog.csdn.net/qin_yu_2010/article/details/81150741

该日志标签: 微软, , 升级, 朋友, 系统, 来源, 注册, 日志, 模板, 数据库, 内容, 方法, 运行, c, mstsc, 界面, 策略, 管理, 攻击, 加密, 问题, 菜单, 更新, 知识库, win10, , credssp, 解决

视频防盗链技术方案研究与讲解

该日志由 samool 发表于 2020-09-15 18:48:00

作者:谭东

最近刚完成点播视频防盗链的部署,也对视频防盗链技术方案有了研究与了解。在这里给大家分享下技术方案和原理。

一、先说下为什么要防盗链?

这个主要是:

  1. 网站方为了防止自己的点播服务器的资源和流量被盗用;
  2. 保护视频版权;
  3. 防止视频被下载盗用等。

二、防盗链技术方案

一般盗链者可以直接获取播放地址盗用或者破解播放器盗用、录屏盗用、下载盗用、视频链接破解提取原地址等方式进行盗链。

所以我们在防盗链时要综合这些因素考虑,当然不可能面面俱到,但是能够覆盖大部分常见的手段即可。

常见方案有:

  1. 视频加水印;
  2. 视频加DRM版权水印;
  3. 视频加密;
  4. 播放器加解密;
  5. Referer防盗链
  6. URL防盗链
  7. 采用m3u8等盗链难度高的文件格式,也可以防下载;
  8. IP黑白名单限制等。

说了这么多,我们来看看目前主流的视频防盗链技术方案,例如央视网、腾讯、优酷等平台一般都采用Referer防盗链和URL防盗链方式进行防盗链。针对直播视频防盗链或点播视频防下载,我们还可以将视频文件格式变成m3u8格式,这样可以防止视频被下载。

那么接下来我们就简单讲解下Referer防盗链和URL防盗链方案:

(1)Referer防盗链

Referer防盗链比较简单,是基于 HTTP 协议支持的 Referer 机制,通过播放请求 Header 中携带的 Referer 字段识别请求的来源。开发者可以设置一批域名为黑名单或白名单,CDN 节点将按照名单中的域名做鉴权,从而允许或拒绝播放请求。也就是携带了播放来源,如果播放这个视频源的页面地址不对的话,则会拒绝播放。当然Referer是可以伪造的,所以建议结合URL防盗链进行使用。

(2)URL防盗链

URL防盗链是主流防盗链方案,也是安全度比较高一种防盗链方式。当然也有称作是Token校验方式,也有叫做鉴权防盗链,这里统称URL防盗链。

URL防盗链可以设置很多功能,如视频链接过期时间、播放人数控制、视频播放时长控制(如试看5分钟这种功能)等等。而控制鉴权的这部分放在CDN节点中去处理。

URL防盗链,允许开发者将视频的播放控制参数以 QueryString 的形式拼接在视频 URL 中,CDN 节点将检查 URL 中的播放控制参数,并依据参数控制视频的播放。目前,Key 防盗链通过“过期时间参数”、“允许播放的 IP 数量参数”和“试看时间参数”,支持“防盗链有效时间控制”、“防盗链播放人数控制”和“视频播放时长控制”。

  • 防盗链有效时间控制

在视频 URL 中指定过期时间。如果请求的视频 URL 已过期,则视频无法播放。通过这种方式,可以为视频 URL 设置有效时间,防范他人将视频 URL 转移到其他站点后长期使用。

  • 防盗链播放人数控制

在视频 URL 中指定链接最多能供多少人播放。不在同一内网的播放终端,它们的公网 IP 一般是不同的。通过限制一个 URL 允许最多能被多少公网 IP 播放,就能够限制同一个 URL 可以播放的人数,从而可以防范他人将视频 URL 转移到其他站点后,无限制地分发给任意多的人数观看。

  • 视频允许播放时长控制

在视频 URL 中指定试看时长(如仅允许播放视频的前5分钟)。通过这种方式,可以实现对未付费用户的试看功能。

URL防盗链功能简介:

  • 支持在视频 URL 中指定过期时间,他人获取后无法长期使用。
  • 支持在视频 URL 中指定最大允许播放 IP 数,他人获取后不能无限制地分发给更多人观看。
  • 支持在视频 URL 中指定试看时长,实现试看功能。
  • 开发者使用密钥KEY对视频 URL 签名,并在 URL 中带上签名结果。只要用户密钥不泄露,其他用户无法伪造视频 URL。
  • CDN 节点检查视频 URL 中的参数和签名,对视频播放请求进行控制。如果请求检查不通过,则返回403响应码。
  • 支持的文件类型:MP4、TS、M3U8、FLV、AAC、MOV、WMV、AVI、MP3、RMVB、MKV、MPG、3GP、WEBM、M4V、ASF、F4V、WAV、MPEG、VOB、RM、WMA、DAT、M4A、MPD、M4S。

三、具体URL防盗链生成和使用

URL的生成是由我们这边服务器处理的,根据指定规则进行生成防盗链处理后的URL。

URL的生成是由我们这边服务器处理的,根据指定规则进行生成防盗链处理后的URL。
URL的生成是由我们这边服务器处理的,根据指定规则进行生成防盗链处理后的URL。

  • 开发者在视频的地址中均存在视频原始 URL。未开启防盗链时,使用视频原始 URL 即可播放视频。
  • 开启 Key 防盗链后,视频原始 URL 不再能播放,此时需要构造视频的防盗链 URL,CDN解析鉴权通过后返回视频数据。

(1)生成URL

防盗链 URL 的生成规则是在原始 URL 尾部,以 QueryString 的方式加入防盗链参数,形如:

http://example.vod.com/dir1/myVideo.mp4?t=[t]&exper=[exper]&rlimit=[rlimit]&us=[us]&sign=[sign]

这几个参数含义:

  • t(必填):播放地址的过期时间戳,以 Unix 时间的十六进制小写形式表示;过期后该 URL 将不再有效,返回403响应码。考虑到机器之间可能存在时间差,防盗链 URL 的实际过期时间一般比指定的过期时间长5分钟,即额外给出300秒的容差时间;建议过期时间戳不要过短,确保视频有足够时间完整播放。
  • exper(选填/选用):试看时长,单位为秒,以十进制表示,不填或者填0表示不试看(即返回完整视频);试看时长不要超过视频原始时长,否则可能导致播放失败。
  • rlimit(选填/选用):最多允许多少个不同 IP 的终端播放,以十进制表示,不填表示不做限制;当限制 URL 只能被1个人播放时,建议 rlimit 不要严格限制成1(例如可设置为3),因为移动端断网后重连 IP 可能改变。
  • us(选填/选用):链接标识,用于随机化一个防盗链 URL,增强链接的唯一性;建议每次生成防盗链 URL 时,指定一个随机的 us 值。
  • sign(必填):防盗链签名,以32个字符长的十六进制数表示,用于校验防盗链 URL 的合法性;签名校验失败将返回403响应码。

签名计算公式如下:

sign = md5(KEY + t + exper + rlimit + us)

有人可能注意到了,怎么多了一个KEY?这个KEY就是我们的一个密钥,CDN和我们生成URL的服务器都要使用相同的KEY才能进行加密和解密配对,开启 URL 防盗链时填写的密钥。必须由大小写字母(a - Z)或者数字(0 - 9)组成,长度在8 - 20个字符之间。

(2)防盗链URL生成示例:

例如视频原始地址:[https://example.vod.com/dir1/myVideo.mp4](),密钥KEY为:24FEQmTzro4V5u3D5epW,随机字符串us为72d4cd1101,URL 的过期时间是2018年01月31日20:00(Unix 时间为1517400000)即t为1517400000这里就不设置exper和limit了。

计算签名:

sign = md5("24FEQmTzro4V5u3D5epW5a71afc072d4cd1101") = "01ad188259e1f34979c06a10e6d0fb89"

最后我们生成URL防盗链:

http://example.vod.com/dir1/myVideo.mp4?t=5a71afc0&us=72d4cd1101&sign=01ad188259e1f34979c06a10e6d0fb89

我们这边服务器自己按照规则生成防盗链后的URL即可,CDN结点也按照这个规则和KEY来进行鉴权即可,匹配即允许播放,否则不允许访问资源。

好了,大致内容就是这么多,是不是很简单?对视频防盗链有了进一步了解?当然如果你想防止下载的话,建议使用HLS(m3u8)文件格式来进行传输,这样可以避免文件下载,安全性更高,而且HLS(m3u8)也可以对分片文件进行标准化或者自定义加密,安全度会更高。

我们这边服务器自己按照规则生成防盗链后的URL即可,CDN结点也按照这个规则和KEY来进行鉴权即可,匹配即允许播放,否则不允许访问资源。

好了,大致内容就是这么多,是不是很简单?对视频防盗链有了进一步了解?当然如果你想防止下载的话,建议使用HLS(m3u8)文件格式来进行传输,这样可以避免文件下载,安全性更高,而且HLS(m3u8)也可以对分片文件进行标准化或者自定义加密,安全度会更高。
我们这边服务器自己按照规则生成防盗链后的URL即可,CDN结点也按照这个规则和KEY来进行鉴权即可,匹配即允许播放,否则不允许访问资源。

好了,大致内容就是这么多,是不是很简单?对视频防盗链有了进一步了解?当然如果你想防止下载的话,建议使用HLS(m3u8)文件格式来进行传输,这样可以避免文件下载,安全性更高,而且HLS(m3u8)也可以对分片文件进行标准化或者自定义加密,安全度会更高。

该日志标签: 字符, 技术, 腾讯, 移动, 来源, 使用, 视频, 网站, 内容, 央视, 文件, 数字, 播放器, 链接, 防盗链, 规则, url, 直播, 流量, 域名, 加密, KEY, 数据, 时间, 长度, 参数, 播放, 失败, 解密, , 用户, 防盗

CentOS7 + 宝塔环境搭建 Gogs 私人Git 仓库

该日志由 samool 发表于 2020-09-01 16:41:00

安装文档 https://gogs.io/docs/installation

我这里的安装环境:

CentOS7 + 宝塔面板 + Nginx + Mysql5.7

git 版本 1.8.3.1

Gogs 版本 0.11.66 @ 2018-09-16

安装包下载地址列表:

官方:

https://gogs.io/docs/installation/install_from_binary.html

github

https://github.com/gogs/gogs/releases

找到 Linux amd64 的下载地址

https://dl.gogs.io/0.11.66/gogs_0.11.66_linux_amd64.zip

安装宝塔面板

**

yum install -y wget && wget -O install.sh http://download.bt.cn/install/install.sh && sh install.sh

我这里选择的环境是

Mysql5.7 + PHP7.2 + Nginx 1.14.0 + phpmyadmin4.x + Pure-Ftpd 1.0.47

安装Git (已经安装的跳过)

**

yum install -y git

查看Git版本

**

git --version

创建一个git用户组

**

groupadd git

创建一个git用户并添加到git用户组

**

useradd -g git git

修改git用户的密码

**

passwd git

创建好了之后, 切换到Git身份

**

su git

这时候已经是git用户了, 进入git的home目录

**

cd ~

下载gogs安装压缩包, 双引号里的下载地址替换为官方最新版本的下载地址

**

wget -c "https://dl.gogs.io/0.11.66/gogs_0.11.66_linux_amd64.zip" -O gogs.zip

解压安装包

**

unzip gogs.zip

进入gogs目录

**

cd gogs

启动安装程序

**

./gogs web
在宝塔面板->安全-> 添加3000端口, 允许访问

回到桌面, 用浏览器打开安装向导页面, 根据自己情况设置

**

http://192.168.1.101:3000

安装完成后, 回到终端, 退出当前的git用户

**

exit

为gogs添加服务,开机启动

**

# 创建服务配置文件
vim /lib/systemd/system/gogs.service

加入以下内容, 保存退出:

**

[Unit]
Description=Gogs
After=syslog.target
After=network.target
After=mysqld.service

[Service]
# Modify these two values and uncomment them if you have
# repos with lots of files and get an HTTP error 500 because
# of that
###
#LimitMEMLOCK=infinity
#LimitNOFILE=65535
Type=simple
User=git
Group=git
WorkingDirectory=/home/git/gogs
ExecStart=/home/git/gogs/gogs web
Restart=always
Environment=USER=git HOME=/home/git

[Install]
WantedBy=multi-user.target

设置开机启动

**

systemctl enable gogs.service

并手动启动服务

**

systemctl start gogs

查看服务状态

**

systemctl status gogs

在宝塔面板上配置域名访问(反向代理)

这个时候就可以通过域名访问了, https证书在宝塔界面配置部署即可, 不用改gogs配置

使用域名访问搭建好的gogs

该日志标签: 代理, 浏览器, 目录, 来源, 服务, 桌面, 内容, 文件, 访问, 密码, 程序, 链接, c, 列表, 安装, 界面, 域名, web, 环境, get, cd, error, 商业, 端口, 宝塔, , 用户, git, gogs, home

Centos7 下安装Golang

该日志由 samool 发表于 2020-09-01 16:39:49

1.下载安装包

下载地址:https://studygolang.com/dl
本次下载 go1.14.1.linux-amd64.tar.gz

wget https://studygolang.com/dl/golang/go1.14.1.linux-amd64.tar.gz

2. 解压到指定目录

tar -C /usr/local -xzf go1.14.1.linux-amd64.tar.gz

解压后在目录 /usr/local/go中

3. 配置环境变量

设置GOPATH 目录

mkdir -p /home/gocode

go命令依赖一个重要的环境变量:$GOPATH

GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号;,Linux系统是冒号:
当有多个GOPATH时默认将go get获取的包存放在第一个目录下
$GOPATH目录约定有三个子目录

src存放源代码(比如:.go .c .h .s等)
pkg编译时生成的中间文件(比如:.a)
bin编译后生成的可执行文件(为了方便,可以把此目录加入到 PATH变量中,如果有多个gopath,那么使用PATH变量中,如果有多个gopath,那么使用{GOPATH//://bin:}/bin添加所有的bin目录)

编辑环境

vim /etc/profile

在最后一行加入 按i插入

export GOROOT=/usr/local/go #设置为go安装的路径
export GOPATH=/home/gocode #默认安装包的路径
export PATH=PATH:GOROOT/bin:$GOPATH/bin

按esc退出插入模式 :wq!强制保存退出

vim命令编辑模式命令如下
:q 不保存退出
:q! 不保存强制退出
:wq 保存退出,w表示写入,不论是否修改,都会更改时间戳
:x 保存退出,如果内容未改,不会更改时间戳
保存后执行 使环境生效

source /etc/profile

该日志标签: 目录, 系统, 内容, 文件, 代码, c, gz, tar, 命令, export, 时间, 保存, 环境, get, 变量, gopath, bin, path

网页复制内容时自动添加版权链接信息

该日志由 samool 发表于 2020-06-19 23:56:00

JS 仿知乎复制文章末尾自动添加字符串
当别人复制你的文章内容时,会在文章末尾自动添加一串字符。
将此代码放置在body之前即可

<!--自动添加版权信息 -->
<script>
$("body").bind('copy', function (e) {
    if (typeof window.getSelection == "undefined") return; //IE8 及更老的版本不兼容
    
    var body_element = document.getElementsByTagName('body')[0];
    var selection = window.getSelection();
    
    //创建一个DIV的可见区域之外
    //并填写选定的文本
    var newdiv = document.createElement('div');
    newdiv.style.position = 'absolute';
    newdiv.style.left = '-99999px';
    body_element.appendChild(newdiv);
    newdiv.appendChild(selection.getRangeAt(0).cloneContents());
    
    //我们需要<pre>标签解决方案
    //其他的文本在<pre>失去了所有的行符!
    if (selection.getRangeAt(0).commonAncestorContainer.nodeName == "PRE") {
        newdiv.innerHTML = "<pre>" + newdiv.innerHTML + "</pre>";
    }
    
    newdiv.innerHTML += "<br /><br />文章来自: 《傻猫日志》 <a href='"
    + document.location.href + "'>"
    + document.location.href + "</a>";
            
    selection.selectAllChildren(newdiv);
    window.setTimeout(function () { body_element.removeChild(newdiv); }, 200);
});
</script>   

该日志标签: 标签, 日志, 内容, 网页, , 链接, 代码, 信息, 复制, 文章, o

用了多年的自动升级小工具《KDBOSS升级助手》

该日志由 samool 发表于 2020-06-17 10:52:00

这个小工具软件好像是8年前做的,那一年公司开年会,我知道我没有中奖命(年会抽奖也重来没有中过,虽然抽奖程序是我们部门搞的,全是随机选择,绝对没有内幕),边看节目边做的小工具软件,用作客户端软件升级多年,相当的稳定和好用,开源出来给需要的朋友吧。

源码下载:自动升级模块源码.rar

主要功能:

  1. 采用idhttp下载
  2. ini作配置文件
  3. 支持下载进度百分比
  4. 支持下载速率显示
  5. 支持自动下载和自动更新

KDBOSS升级助手.png

阅读剩余部分...

该日志标签: 升级, 朋友, 软件, 网络, 工具, 日志, 内容, 文件, 程序, 抽奖, 日期, idhttp, indy

《Google Adsense的秘密》第八章:内容建设

该日志由 samool 发表于 2007-09-29 13:29:13

8.1 自创内容

Google Adsense比其他的在线广告系统更好的原因之一是:它的广告与你网页上的内容是相关的,用户点击这些广告是因为对它们有兴趣。并且用户们会回来再次点击它们,因为你的内容是有吸引力的。

如果你网站的内容缺乏吸引力,你就会为如何吸引用户和链接而绞尽脑汁,这样你就没办法让用户成为回头客。

有”正确”的内容,是通过Google Adsense获得好收入的重要条件,同时,也是让Google的搜索系统对你的网站有好印象的重要条件。要知道, Google首先是一个搜索引擎,它的使命是按照用户的搜索指令提供最好的搜索结果。如果你提供高质量的内容,你就有极大的可能让自己的网站出现在搜索结果的前列。

幸运的是,为你的网站制造一页一页的内容,并且在每一页上放置广告来制造赚钱的机会,已经是一件越来越容易的事情。

最显而易见的制造内容的办法当然就是”自己写”。找一个你喜欢的主题,用心写吧。比如,如果你是电脑游戏专家,你就可以建立一个网站,放上相关的心得体会、新闻和攻略,当然都是您自己写的。随后Adsense就会把相关的广告给你,只要你把它们放置正确,它们就会带给你收入,也许这些收入足够支付你的游戏费用。只要你想,你可以按照上面的办法做任何主题的网站

但是记住,你建立网站的目的是为了赚钱,那么你自己创作这些内容就意味着你在为赚这些钱而工作。当你数钱时,可别忘了你为这些钱所付出的时间和精力。

所以,更多的人在寻找更容易,更简单的办法来获得内容(事实上,很多人都不喜欢自己动笔写作)。幸运的是,有很多毫不费力的办法可以获得内容,并且其中一些办法是免费的。

8.2 用博客赚钱

准确地说,写博客可不是件毫不费力的事,但是很多人都喜欢做,并且因为博客是每天更新的,Google也很喜欢博客。如果你打算写博客,你当然可以用它赚钱。

写博客赚钱最大的挑战,是如何获得高价值的广告。通常你的博客是用来谈论所有你感兴趣的不同种类的事情,所以你就可能随机地得到不同种类的广告。这样也挺好,除非你觉得这些广告带来的收入甚至不足以支付你写博客的费用。

如果你对博客上随机的广告忍受够了,你可以试着修改你模板中的meta 标签来改善,删掉<$Metainfodata$>标签,用你希望的关键词和网站描述来代替它,如下:

  1. <meta name="robots" content="index,follow">
  2. <meta name="keywords" Content="你的关键词">
  3. <meta name="description" Content="富含关键词的网站描述">

要确保你选择的这些关键词在你的博客中大量出现,并且多设置一些包含这些关键字的标题,在整个博客中不断地重复这些关键字。

最重要的一点,要确保你的博客中有大量的文字内容,用你的朋友的、家庭的和宠物的照片把博客填满应该很有趣,不过Google可读不懂这些,这样干的话你的博客上恐怕就要招来公益性广告了。

8.3 如何在你的博客中加入Google Adsense广告

由于博客站点使用的模板不同,所以如何添加Adsense广告要取决于你使用哪家的博客服务,具体做法建议向你的博客提供商咨询。

8.4 往日文章

博客需要你一直地写,但是如果你过去写过一些文章地话,不要让它们趴在书架上晒太阳,把这些老作品扔到你的博客上来让它们焕发第二春吧。

举个例子,”低脂Linux”是Bob Rankin几年前的作品,你可以在Amazon.com找到它,但是看起来买这本书的人并不多,因为你可以在www.lowfatlinux.com免费读到它。Bob的这本书已经完成了在书店出售的任务,现在它开始了第二个任务:为广告吸引点击。现在,去看看你的储物柜里有没有什么能让你挣钱的东西。

google adsense的秘密,博客联盟,博客赚钱

如图,Bob Rankin用以往的作品赚钱。注意他摆放广告的位置,这些广告很不错但是为什么不把它们放在右边从而得到更多的点击呢?他还应该在左边的链接上部放一个Adsense的链接单元广告。

你可能有一本卖得不怎么样的电子书,与其尝试着每本卖19.95美元,还不如把它放到网页上与大家免费分享。同时,把Adsense广告代码放在网页上,也许你得到的收入比你直接卖掉它还多。把这些老作品废物利用,是个挖掘自身赚钱潜力的好方法。

8.5 自愿作者

想用老作品赚钱,你起码得有老作品,如果恰好没有,而且你也不打算自己写些新东西,那么还有一个可行的办法-让别人免费为你写文章。

很多人喜欢写东西,看看Amazon吧,Amazon从不为书评掏一个子儿,人们免费写书评但Amazon却从中受益。当我启动www.WorldVillage.com时,我可没有钱支付给那些为我写软件评论的用户。我联系游戏公司取得免费赠送的电脑游戏,然后交给那些自愿的试用者,这些试用者可以保留这些游戏,唯一的要求是他们要提供一份游戏评论。靠这个办法,我得到了几十份游戏评论,这些游戏评论为我的网站带来了流量和广告点击。我并没有付出现金,但是几年后,这些评论仍旧在带给我收入。

你并不需要也用这一招,不管你的站点的主题是什么,你总可以找到办法让人们免费为你贡献思考和评论。你可以说”我们需要你!我们需要你的思想、文章和评论。把你的作品发送到editor@yoursite.com,我们就会在网站发表。”这样你就可以创建许多新的网页,并在上边放置Adsense广告。

google adsense的秘密,博客联盟,博客赚钱

如图,http://www.worldvillage.com/softwarereviews/index.html上的游戏评论,呵呵,它们可是免费得来的。

8.6 用别人的内容建造海量的网页

你网站关注的焦点是什么?子女教育?金融理财?网站导航?或者是关于法律事务的?不管怎样,你都可以从以下这个小秘密中受益:免费的联合发布。

很多作者对他们的作品除了发表和被传阅之外,别无所求。联合发布对作者和发布者都是个美梦成真的好事,对作者来说,联合发布把他的作品向大众展示,对发布者(就是你)来说,这意味着为你的网站带来高质量的内容。你可能还不知道,网络上有着大量的免费文章可以用来让你充实你的网站。

当然,关键是在哪里找到这些文章。下面给出的一个名单,可以让你找到超过3万篇免费文章来充实你的网站。

请注意,下述的每个网站对关于如何使用它们的内容都有自己的规定,一般来说,你必须注明作者的姓名并给出原文的完整链接。有的网站需要你在你的网站上添加它们站点的链接。这些很重要!记住,当你被允许使用这些内容的时候,它们的知识产权仍属于作者,请尊重作者的权利。

你可能只想发布那些与你网站内容相关的文章,或者你想成为一个大杂烩网站,把成百上千的文章都添加进去。不论怎样,下面的这几个网站都可以提供数千的免费文章供你使用。(下面这些网站提供的都是英文文章,对国内的站长帮助不大-老剑注)

EzineArticles.com
http://www.ezinearticles.com

DotComWomen.com
http://www.dotcomwomen.com/free-content.shtml

John Watson
http://members.tripod.com/buckcreek

ValuableContent.com
http://www.valuablecontent.com/

Patricia Fripp
http://www.fripp.com/articleslist.html

ArticleCity.com
http://www.articlecity.com

还想要更多文章么?只要简单地在Google上搜索”免费文章”,然后看看搜出来什么即可。通过调整你的搜索关键词,可以得到更多符合你需求的免费资源,比如”免费的子女教育文章”,或者”免费的金融理财文章”。

现在,你已经知道去哪里得到免费内容了,你可以制造成百上千与你的关键词相关的网页了,你只要在这些网页上放置Google Adsense广告,就可以等着数钱了。

8.7 把公有领域的作品添加到你的网站

一个关于免费内容的小秘密是有关公有领域的,一般来说,这里有很多版权过期的图书、文章、音像资料和图片等等。这些作品没有进行版权的再次注册,所以进入了公有领域。这意味着什么?这意味着你可以发布、发表甚至出版这些作品而不用支付任何费用。

想想吧,只要一本公有的图书,就能让你在网站里添加上百个网页!想想这将为你带来多少对Adsense广告的注目吧,这种可能简直是无限的。

我这里有两个你想要的资源,里面有很多你立刻就可以使用的公有作品。

点子 #1 - 古腾堡项目(Gutenberg,古腾堡,是德国活版印刷的发明人-老剑注)

古腾堡项目是互联网上一个历史最久的免费电子图书项目,数百的志愿者为这个项目收录了超过15000本电子书。以下是古腾堡项目中10本最流行的作品:

1. The Notebooks of Leonardo Da Vinci by Leonardo da Vinci
2. Project Gutenberg “10K” DVD
3. How to Live on 24 Hours a Day by Arnold Bennett
4. The Art of War
5. Ulysses by James Joyce
6. The Adventures of Sherlock Holmes by Sir Arthur Conan Doyle
7. Project Gutenberg “Best Of” CD August 2003
8. How to Speak and Write Correctly by Joseph Devlin
9. Alice’s Adventures in Wonderland by Lewis Carroll
10. Grimm’s Fairy Tales by Jacob Grimm and Wilhelm Grimm

古腾堡项目的网址:http://www.gutenberg.org/。

请注意,你可以在你的网站上发布这些作品,但是不可以出售这些作品以牟利。一个不利之处是,你想用的作品可能已经有数百人捷足先登了。你可以尝试用一些不太流行的作品以换取在搜索引擎中比较好的位置。

点子 #2 - 失踪的文件

“失踪的文件”计划,创始人是Russell Brunson,这是一个依靠捐助的资金而将比较新的作品放入公有领域的计划。换句话说,它可以让你在每个人都拥有之前将一些新书或文章发布!下面是一些对会员开放的图书目录:

* How To Develop Self Confidence
* How To Draw and Paint
* Guide To Bowling
* What Character Does Your Handwriting Reveal
* How To Develop Perseverance
* How To Become A Speaker
* Dreams Interpreted
* How To Play Tennis
* Hints On Writing Short Stories
* How To Make Money
* Strength From Eating
* How To Study The Bible For Greater Profit
* The History Of Music
* How To Make and Use A Home Radio
* How To Write A Hit Song And Sell It

我已经对这个计划进行了捐助,并且计划在我的一个网站上尝试发布一些这里的作品。

8.8 在RSS列表上放置Google Adsense广告

最近互联网上最大的变化就是RSS的快速风行。RSS让订阅者可以随时得知某网站的更新信息,而不是象以往那样必须亲自对该网站进行访问。

Google开始许可在RSS列表上发布Adsense广告,这是个好消息,在我写作本书的同时,Google正在对这个计划进行测试。如果你的网站开启了RSS功能,并且已经有了上百个订阅者,你的网站就可以加入这项计划的Beta版测试。

看起来在RSS列表上优化广告的余地似乎很小,不过象在网页上那样试图把广告融入页面内容应该是可行的。按照Google要求,广告只能出现在内容之后,并且是在链接的下面。你可以在下面这个地址申请加入这个测试:

http://services.google.com/ads_inquiry/aff

如果你不想做Google实验室里的小白鼠,也可以考虑加入在这方面技术领先的Kanoodle,他们的地址是:

http://www.kanoodle.com/about/brightads.cool

8.9 用你的新闻通讯换取流量

一个做得不错的新闻通讯系统,也可以通过Adsense赚钱。新闻通讯是吸引浏览者重复访问你的网站的好工具。办法是这样的:不要把所有的新闻都在 Email里告诉读者,给你的网站留点有吸引力的内容同时提供一个链接,当订阅者点击这些链接的时候,就可能点击网页上的Adsense广告,这就意味着又一笔广告收入。例如,Prizepot是一个关于竞赛和彩票的网站,他们每周免费的新闻通讯上都会提供类似的链接,为了获得最新的情报,你必须点击这些链接。当然,当你打开目标网页时,迎接你的不仅是有关竞赛的情报,还有 Adsense广告。如果你想看看他们是怎么做的,不妨发送Email到joinprizepot@lists.worldvillage.com来订阅一份。

提示:如果你拥有一个庞大的响应及时的Email地址列表,开始让它为你赚取Google Adsense现金吧!

如果这还不足以说服你开始制造一个新闻通讯系统,那么我告诉你Google已经开始尝试在新闻通讯上直接放置Adsense广告了。Google已经与iVillage.com开始这项合作,很可能其他用户也将很快从中受益。

你可以自己撰写一个新闻通讯,并通过群发软件把它发送出去,或者你也可以请求他人为你撰写通讯。ConstantConversions.com就是一家专业的软广告文撰写公司,他们特别擅长于撰写新闻通讯。你可以向他们介绍你的网站并提出要求,他们会把一切搞定,从概念设计到邮件发出。你甚至可以要求他们,内容要尽量适应Adsense的要求,他们在这方面很有经验。

如果你想建立起自己的邮件通讯和邮件自动应答系统,我强力推荐你考虑Aweber.com。有了它,你可以建立起无数的邮件列表和无数的自动应答系统。就是说,你可以预先设定为你不同的邮件列表设置不同的自动发送的邮件序列。Aweber一直是我邮件通讯的不二之选。

8.10 买内容/雇写手

用免费的的内容可以节省成本,不过也有问题。如果你的网站内容质量不高,就会影响广告的点击率。一个可以考虑的办法是花钱请专业人士来为你撰写文章。我曾经提到过www.elance.com,你可以在这里找到专业的人士为你设计网站,同时也可以找到专业的写手来为你的网站写文章。找专业写手来写文章的好处是你可以确保网站内容的高质量,而成本并不会很高。不过当然,你还是必须考虑要收回成本。

不妨做个测试,请一位写手写一些文章,如果你为这些文章支付了200美金但是发现收入并没有提高200美金,那么你要么换个方法,要么换个写手吧。

8.11 自动制造内容

最后,再介绍两个可以为你带来网站内容的选择:ArticleBot和TrafficEqualizer。这两个工具都可以自动的按照你定义的关键词为你制造充满着内容的网页,既省事又省钱。不过,Google可不喜欢这些工业化制造的内容,你的用户也不太可能喜欢。这种做法只是聊胜于无而已,记住,好的内容才是一切!

该日志标签: 秘密, google, adsense, 内容