最近弄到了一个安卓系统的老年机,模仿的是Nokia基于Kaios的3310机型的重置版,这样的一台小屏幕老年机如果能跑主线Linux应该挺有趣的。

信息收集


这台手机具体型号是广信EF33,经过一番调查,一些基本的信息如下。

  • msm8909 骁龙210 4核A7(arm32)
  • 512MB RAM & 4GB ROM
  • 大喇叭+耳机孔
  • 单摄
  • 没有传感器
  • 没有触摸屏的小lcd
  • 没有 Secure Boot
  • 2.4 Ghz Wifi (wcn3610v1)

本来想通过原厂的fastboot来把内核调起来再去想替换Bootloader的事情,先去试试fastboot下有哪些指令能用,没想到的是厂家禁用了fastboot模式下所有的指令,就连reboot指令都使用不了,这意味着想要把主线linux跑起来,就必须先把lk移植上。

$ fastboot reboot
Rebooting                                          FAILED (remote: 'unknown command')
fastboot: error: Command failed

进入到系统,得到root权限后挂载debugfs,debugfs里的gpio文件也可以得到一些外设连接的信息。

$ mount -t debugfs debugfs /sdcard/debug
$ cat /sdcard/debug/gpio
<省略一些不必要的数据>
GPIOs 911-1023, platform/1000000.pinctrl, msm_tlmm_gpio:gpio-911 (camera_vana_2v8     ) out logpio-912 (PA_ENABLE           ) out logpio-925 (matrix_kbd_col      ) out logpio-926 (matrix_kbd_col      ) out logpio-927 (matrix_kbd_col      ) out logpio-928 (dc-gpios            ) out higpio-933 (matrix_kbd_col      ) out logpio-942 (flashlight          ) out logpio-943 (matrix_kbd_col      ) out logpio-947 (matrix_kbd_row      ) in  higpio-949 (matrix_kbd_row      ) in  higpio-963 (button-backlight    ) out logpio-1001 (volume_up           ) in  higpio-1002 (sos_key             ) in  higpio-1006 (screen_lock_irq_gpio) in  logpio-1008 (matrix_kbd_row      ) in  higpio-1009 (matrix_kbd_row      ) in  higpio-1010 (troch               ) out logpio-1021 (matrix_kbd_row      ) in  higpio-1022 (red                 ) out logpio-1023 (green               ) out hi

大体上知道数字键盘是纯gpio实现的,键盘驱动部分也可以使用主线linux现成的驱动,把外设基本跑起来应该问题不大。
通过lk向内核的参数也能确定一些信息,比如屏幕类型(需要root)。

$ cat /proc/cmdline
sched_enable_hmp=1 console=ttyHSL0,115200,n8 androidboot.console=ttyHSL0
androidboot.hardware=qcom user_debug=31 msm_rtb.filter=0x3F
ehci-hcd.park=3 androidboot.bootdevice=7824900.sdhci lpm_levels.sleep_disabled=1 earlyprintk androidboot.emmc=true androidboot.serialno=245df60
androidboot.baseband=msm
mdss_mdp3.panel=0:spi:0:qcom,mdss_spi_st7789v_ctc_qvga_nrx_cmd

不难看出这个手机用的是一块st7789v的spi屏。

继续在某网站上搜索一些猛士上传的文档,可以看出msm8909和msm8916的差别其实不是很大,主要是显示处理模块由主线能驱动的MDP5换成没驱动MDP3,可能把mipi驱动起来还是比较困难的,不过好在这个机器使用的是spi屏,应该能够通过tinydrm把屏幕跑起来。

使用edl把手机的固件导出来,得到lk的二进制文件,高通的Bootloader都是带ELF头的,使用反编译工具就可以直接根据函数符号来确定功能,这使逆向工作容易了许多,用Ghidra能快速锁定这个屏幕的初始化指令涉及到的数据结构。

另外一些信息在设备树里面,可以通过解包boot.img得到,这里就不再赘述。
至此,基本的信息收集工作就完成了,接下来就是lk的移植。

移植Littlekernel Bootloader

首先为了验证msm8909是否和msm8916用的是一套签名机制,这里先使用qtestsign工具对edl提取出的sbl1进行重新签名,这样即使签名不对也会进入9008模式,不至于使用工程线才能救回。(然而之后调spi的时候把lk调崩了还是花15买了条工程线。。。)

$  ./qtestsign.py sbl1 sbl1.bin
$ edl w sbl1 sbl1.bin
$ edl reset

重启后手机正常亮屏进入系统,证明了msm8909的Bootloader部分也可以使用qtestsign工具进行签名。
ArchLinux上的gcc11编译一些老项目时会有各种问题,这里就不再使用高通官方开源的littlekernel,而是lk2nd的expermental-rebase分支,虽然lk2nd部分还没有彻底完工,但是一些gcc版本相关的修复让整个开发过程的麻烦减少很多。

拉下来直接编译msm8909的工程,先直接不做任何修改签名后在实机上跑跑看,遇到问题再去一个个修复。

$ make TOOLCHAIN_PREFIX=arm-none-eabi- msm8909 -j8

得到的产物emmc_appsboot.mbn会出现在build-msm8909文件夹里。
用qtestsign签名后使用edl烧入aboot分区,手机白屏,fastboot所有指令能够正常使用,但是重启不了。
观察lk的相关日志得知是TrustZone的问题,找到一个TrustZone版本号较高的随身wifi的备份刷入后除屏幕外一切正常。

接下来就是如何为这个lk加入一块新的spi屏。
在各大网站里翻了翻,做过高通方案的大佬们貌似都是用高通给的工具通过xml直接生成的,msm8916-mainline项目也有类似的项目linux-mdss-dsi-panel-driver-generator能通过设备树直接生成主线内核用的屏幕驱动和lk用的头文件,查看源码后发现不支持spi屏的转换,也只能照着其他类似的头文件用ghidra逆向的结果一条条手搓了。

没有啥逆向的经验,找到lk中已经编译进去但是没有使用过的屏幕与已知的头文件进行对比,找出编译后这些结构的储存规律。
就以st7789v_qvga这块屏为例,在头文件中,用于表示颜色的结构体数据如下。

static struct color_info st7789v_qvga_cmd_color = {24, 0, 0xff, 0, 0, 0
};

在ghidra中相关数据结构如下所示。

不难看出结构体成员编译后的储存方式,按照这个规律边猜边做完成了另外几个结构体的逆向。
但是在ghidra中的数据虽然结构体成员这种挺好分析出来值,像初始化指令数组这种成员个数不确定的数据结构给我造成了一定的困难。

比如这样的初始化指令

static char st7789v_qvga_cmd_on_cmd1[] = {0x36,0x00,
};

反编译后的结果如下

不过它是用C语言写的,就必然有一个数据结构是描述它的长度的。
分析已知的源代码,发现所有的初始化指令被集中储存于XXX_on_command结构体数组中。

static struct mdss_spi_cmd st7789v_qvga_cmd_on_command[] = {{0x01, st7789v_qvga_cmd_on_cmd0, 0x78},{0x02, st7789v_qvga_cmd_on_cmd1, 0x00},{0x02, st7789v_qvga_cmd_on_cmd2, 0x00},{0x01, st7789v_qvga_cmd_on_cmd3, 0x00},{0x02, st7789v_qvga_cmd_on_cmd4, 0x00},{0x02, st7789v_qvga_cmd_on_cmd5, 0x00},{0x02, st7789v_qvga_cmd_on_cmd6, 0x00},{0x02, st7789v_qvga_cmd_on_cmd7, 0x00},{0x02, st7789v_qvga_cmd_on_cmd8, 0x00},{0x02, st7789v_qvga_cmd_on_cmd9, 0x00},{0x03, st7789v_qvga_cmd_on_cmd10, 0x00},{0x02, st7789v_qvga_cmd_on_cmd11, 0x00},{0x0F, st7789v_qvga_cmd_on_cmd12, 0x00},{0x0F, st7789v_qvga_cmd_on_cmd13, 0x00},{0x01, st7789v_qvga_cmd_on_cmd14, 0x78},{0x01, st7789v_qvga_cmd_on_cmd15, 0x00},{0x01, st7789v_qvga_cmd_on_cmd16, 0x00},
};

最后,通过mdss_spi_cmd这个结构体的定义确定了第一个成员就是一条初始化指令的长度。

struct mdss_spi_cmd {int size; //  <-- 长度char *payload;int wait;uint8_t cmds_post_tg;
};

虽然,确定了长度的储存位置,但此时长度在这个结构体数组里仍然很难提取出,仔细分析之后发现还是有规律可循的。(就是我找不到这个规律就是了~~)

于是,切换思路对设备树下手。
在设备树中,st7789v_qvga的初始化参数如下:

qcom,mdss-spi-on-command = [96 01 1100 02 36 0000 02 3A 0500 02 35 0000 06 B2 0C 0C 00 33 3300 02 B7 7500 02 BB 3D00 02 C2 0100 02 C3 1900 02 04 2000 02 C6 0F00 03 D0 A4 A100 0F E0 70 04 08 09 09 05 2A 3341 07 13 13 29 2F00 0F E1 70 03 09 0A 09 06 2B 3441 07 12 14 28 2E00 01 2100 01 2900 05 2A 00 00 00 EF00 05 2B 00 00 00 EF00 01 2C];

观察lk中的头文件后,大体了解初始化指令的结构为以下形式
<wait> + <size> + <payload>
不难得出所有的屏幕初始化指令的长度。

弄出了屏幕初始化参数头文件后,在target/msm8909/oem_panel.c中加入头文件的定义

@@ -54,6 +56,8 @@#include "include/panel_auo_390p_cmd.h"#include "include/panel_st7789v2_qvga_spi_cmd.h"#include "include/panel_gc9305_qvga_spi_cmd.h"
+#include "include/panel_st7789v_qvga_spi_cmd.h"
+#include "include/panel_st7789v_ctc_qvga_nrx_cmd.h"#pragma GCC diagnostic pop#define DISPLAY_MAX_PANEL_DETECTION 2
@@ -86,6 +90,8 @@ enum {AUO_390P_CMD_PANEL,ST7789v2_QVGA_SPI_CMD_PANEL,GC9305_QVGA_SPI_CMD_PANEL,
+  ST7789V_QVGA_SPI_CMD_PANEL,
+  ST7789V_CTC_QVGA_NRX_CMD_PANEL,UNKNOWN_PANEL};@@ -108,10 +114,23 @@ static struct panel_list supp_panels[] = {{"auo_390p_cmd", AUO_390P_CMD_PANEL},{"ST7789V2_qvga_cmd", ST7789v2_QVGA_SPI_CMD_PANEL},{"gc9305_qvga_cmd", GC9305_QVGA_SPI_CMD_PANEL},
+  {"st7789v_qvga_cmd", ST7789V_QVGA_SPI_CMD_PANEL},
+  {"st7789v_ctc_qvga_nrx_cmd", ST7789V_CTC_QVGA_NRX_CMD_PANEL},};

init_panel_data()中将屏幕结构体指针相应成员和前面逆向出来头文件里的数据结构对应上。

+   case ST7789V_CTC_QVGA_NRX_CMD_PANEL:
+      panelstruct->paneldata       = &st7789v_ctc_qvga_nrx_cmd_panel_data;
+      panelstruct->panelres        = &st7789v_ctc_qvga_nrx_cmd_panel_res;
+      panelstruct->color           = &st7789v_ctc_qvga_nrx_cmd_color;
+      panelstruct->panelresetseq   = &st7789v_ctc_qvga_nrx_cmd_reset_seq;
+      panelstruct->backlightinfo   = &st7789v_ctc_qvga_nrx_cmd_backlight;
+      pinfo->spi.panel_cmds        = st7789v_ctc_qvga_nrx_cmd_on_command;
+      pinfo->spi.num_of_panel_cmds    = ST7789V_CTC_QVGA_NRX_CMD_ON_COMMAND;
+      pan_type = PANEL_TYPE_SPI;
+      break;

最后,在oem_panel_select()中修改屏幕的选择逻辑,强制选择前面逆向出来的屏幕。

@ -502,20 +544,22 @@ int oem_panel_select(const char *panel_name, struct panel_struct *panelstruct,uint32_t platform_type = board_platform_id();uint32_t platform_subtype = board_hardware_subtype();int32_t panel_override_id;
+  int oem_panel_id = oem_read_panel_id();
+  dprintf(INFO, "[HACK] oem_panel_id :%d\n",oem_panel_id);- if (panel_name) {+  if (1) {panel_override_id = panel_name_to_id(supp_panels,
-               ARRAY_SIZE(supp_panels), panel_name);
+              ARRAY_SIZE(supp_panels), "st7789v_ctc_qvga_nrx_cmd");if (panel_override_id < 0) {dprintf(CRITICAL, "Not able to search the panel:%s\n",
-                    panel_name);
+                   "st7789v_ctc_qvga_nrx_cmd");} else if (panel_override_id < UNKNOWN_PANEL) {/* panel override using fastboot oem command */panel_id = panel_override_id;dprintf(INFO, "OEM panel override:%s\n",
-                   panel_name);
+                  "st7789v_ctc_qvga_nrx_cmd");goto panel_init;}}

烧录进机器后,开机仍然是白屏,但是log已经显示逆向出来的屏幕被选择并初始化了。

这个阶段僵持了比较久的时间,主要是认为手机厂商跑spi屏一定会用公版设计,没有怀疑lk默认的屏幕spi接口和这个手机不一样(关键是背光亮了。。),在翻了lk中spi屏幕初始化的一整套过程后,尝试了很多种方法,仍然是白屏,最后看反编译的设备树,发现屏幕接在BLSP1的QUP5上,而lk2nd的是QUP4。(BLSP是高通搞得一种黑科技,可以将QUP配成i2c、spi等各种低速接口,但是一个QUP只能配置一种功能)

         qcom,mdss_spi_client {reg = <0x00>;compatible = "qcom,mdss-spi-client";label = "MDSS SPI QUP5 CLIENT"; // 《--- QUP5dc-gpio = <0x95 0x11 0x00>;spi-max-frequency = <0x2faf080>;};

所以只需要加入QUP5的spi支持就可以了。但是lk2nd并没有定义qub5上的spi,照着寄存器手册试着写了一下。

diff --git a/platform/msm8909/include/platform/iomap.h b/platform/msm8909/include/platform/iomap.h
index 59919972..fe3a2dec 100755
--- a/platform/msm8909/include/platform/iomap.h
+++ b/platform/msm8909/include/platform/iomap.h
@@ -152,6 +152,13 @@#define GCC_BLSP1_QUP5_SPI_APPS_N           (CLK_CTL_BASE + 0x6030)#define GCC_BLSP1_QUP5_SPI_APPS_D           (CLK_CTL_BASE + 0x6034)+#define GCC_BLSP1_QUP6_SPI_APPS_CBCR     (CLK_CTL_BASE + 0x701C)
+#define GCC_BLSP1_QUP6_SPI_APPS_CMD_RCGR  (CLK_CTL_BASE + 0x7024)
+#define GCC_BLSP1_QUP6_SPI_CFG_RCGR       (CLK_CTL_BASE + 0x7028)
+#define GCC_BLSP1_QUP6_SPI_APPS_M             (CLK_CTL_BASE + 0x702C)
+#define GCC_BLSP1_QUP6_SPI_APPS_N             (CLK_CTL_BASE + 0x7030)
+#define GCC_BLSP1_QUP6_SPI_APPS_D             (CLK_CTL_BASE + 0x7034)
+/* GPLL */#define GPLL0_STATUS                (CLK_CTL_BASE + 0x21024)#define GPLL0_MODE                  (CLK_CTL_BASE + 0x21000)
diff --git a/platform/msm8909/msm8909-clock.c b/platform/msm8909/msm8909-clock.c
index cfca2bdc..a175b064 100644
--- a/platform/msm8909/msm8909-clock.c
+++ b/platform/msm8909/msm8909-clock.c
@@ -657,6 +657,33 @@ static struct branch_clk gcc_blsp1_qup5_spi_apps_clk = {},};+static struct rcg_clk gcc_blsp1_qup6_spi_apps_clk_src =
+{+  .cmd_reg      = (uint32_t *) GCC_BLSP1_QUP6_SPI_APPS_CMD_RCGR,
+  .cfg_reg      = (uint32_t *) GCC_BLSP1_QUP6_SPI_CFG_RCGR,
+  .m_reg        = (uint32_t *) GCC_BLSP1_QUP6_SPI_APPS_M,
+  .n_reg        = (uint32_t *) GCC_BLSP1_QUP6_SPI_APPS_N,
+  .d_reg        = (uint32_t *) GCC_BLSP1_QUP6_SPI_APPS_D,
+  .set_rate     = clock_lib2_rcg_set_rate_mnd,
+  .freq_tbl     = ftbl_gcc_blsp1_qup1_spi_apps_clk,
+  .current_freq = &rcg_dummy_freq,
+
+  .c = {+      .dbg_name = "gcc_blsp1_qup6_spi_apps_clk_src",
+      .ops      = &clk_ops_rcg,
+  },
+};
+
+static struct branch_clk gcc_blsp1_qup6_spi_apps_clk = {+  .cbcr_reg = GCC_BLSP1_QUP6_SPI_APPS_CBCR,
+  .parent   = &gcc_blsp1_qup6_spi_apps_clk_src.c,
+
+  .c = {+      .dbg_name = "gcc_blsp1_qup6_spi_apps_clk",
+      .ops      = &clk_ops_branch,
+  },
+};
+/* Display clocks */static struct clk_freq_tbl ftbl_mdss_esc0_1_clk[] = {F_MM(19200000,    cxo,   1,   0,   0),
@@ -814,6 +841,8 @@ static struct clk_lookup msm_clocks_msm8909[] =CLK_LOOKUP("gcc_blsp1_qup4_spi_apps_clk", gcc_blsp1_qup4_spi_apps_clk.c),CLK_LOOKUP("gcc_blsp1_qup5_spi_apps_clk_src", gcc_blsp1_qup5_spi_apps_clk_src.c),CLK_LOOKUP("gcc_blsp1_qup5_spi_apps_clk", gcc_blsp1_qup5_spi_apps_clk.c),
+  CLK_LOOKUP("gcc_blsp1_qup6_spi_apps_clk_src", gcc_blsp1_qup6_spi_apps_clk_src.c),
+  CLK_LOOKUP("gcc_blsp1_qup6_spi_apps_clk", gcc_blsp1_qup6_spi_apps_clk.c),CLK_LOOKUP("mdp_ahb_clk", mdp_ahb_clk.c),CLK_LOOKUP("mdss_esc0_clk", mdss_esc0_clk.c),

另外还需要在platform/msm8909/gpio.c中把原来的qup5上的i2c功能取消掉,并初始化qup5的spi引脚。

--- a/platform/msm8909/gpio.c
+++ b/platform/msm8909/gpio.c
@@ -124,13 +124,6 @@ void gpio_config_blsp_i2c(uint8_t blsp_id, uint8_t qup_id)GPIO_8MA, GPIO_DISABLE);break;case QUP_ID_5:
-               /* configure I2C SDA gpio */
-               gpio_tlmm_config(18, 3, GPIO_OUTPUT, GPIO_NO_PULL,
-                   GPIO_8MA, GPIO_DISABLE);
-
-               /* configure I2C SCL gpio */
-               gpio_tlmm_config(19, 3, GPIO_OUTPUT, GPIO_NO_PULL,
-                   GPIO_8MA, GPIO_DISABLE);break;default:
@@ -147,51 +140,33 @@ void gpio_config_blsp_spi(uint8_t blsp_id, uint8_t qup_id){if(blsp_id == BLSP_ID_1) {switch (qup_id) {-case QUP_ID_3:
-               /* configure SPI MOSI gpio */
-               gpio_tlmm_config(12, 1, GPIO_OUTPUT, GPIO_NO_PULL,
-                   GPIO_16MA, GPIO_DISABLE);
-
-                   /* configure SPI MISO gpio */
-               gpio_tlmm_config(13, 1, GPIO_OUTPUT, GPIO_NO_PULL,
-                   GPIO_16MA, GPIO_DISABLE);
-
-               /* configure SPI CS_N gpio */
-               gpio_tlmm_config(14, 1, GPIO_OUTPUT, GPIO_NO_PULL,
-                   GPIO_16MA, GPIO_DISABLE);
-
-               /* configure SPI CLK gpio */
-               gpio_tlmm_config(15, 1, GPIO_OUTPUT, GPIO_NO_PULL,
-                   GPIO_16MA, GPIO_DISABLE);break;
-case QUP_ID_4:
+          break;
+          case QUP_ID_0:
+              break;
+          case QUP_ID_1:
+              break;
+          case QUP_ID_2:
+              break;
+          case QUP_ID_5:/* configure SPI MOSI gpio */
-               gpio_tlmm_config(16, 1, GPIO_OUTPUT, GPIO_NO_PULL,
+              gpio_tlmm_config(8, 1, GPIO_OUTPUT, GPIO_NO_PULL,GPIO_16MA, GPIO_DISABLE);/* configure SPI MISO gpio */
-               gpio_tlmm_config(17, 1, GPIO_OUTPUT, GPIO_NO_PULL,
+              gpio_tlmm_config(9, 1, GPIO_OUTPUT, GPIO_NO_PULL,GPIO_16MA, GPIO_DISABLE);/* configure SPI CS_N gpio */
-               gpio_tlmm_config(18, 1, GPIO_OUTPUT, GPIO_NO_PULL,
+              gpio_tlmm_config(10, 1, GPIO_OUTPUT, GPIO_NO_PULL,GPIO_16MA, GPIO_DISABLE);/* configure SPI CLK gpio */
-               gpio_tlmm_config(19, 1, GPIO_OUTPUT, GPIO_NO_PULL,
+              gpio_tlmm_config(11, 1, GPIO_OUTPUT, GPIO_NO_PULL,GPIO_16MA, GPIO_DISABLE);
-           break;
-
-           case QUP_ID_0:
-           case QUP_ID_1:
-
-           case QUP_ID_2:
-
-
-
-           case QUP_ID_5:
+              break;default:dprintf(CRITICAL, "Incorrect QUP id %d\n",qup_id);ASSERT(0);

最后修改target/msm8909/include/target/display.h中定义的qup_id

-#define SPI_QUP_ID                   4
+#define SPI_QUP_ID                   5

编译,签名,烧录,企鹅!

至此,一个调试用的Bootloader大功告成,接下来就是主线内核的移植了!

记一个老年机的逆向工程与主线linux移植 (一)—— 信息收集与Bootloader移植相关推荐

  1. 记一个老年机的逆向工程与主线linux移植 (二)—— 主线内核和postmarketOS

    现在,这台手机已经有了一个可以调试的Bootloader.接下来可以去搜索以下其他大佬有没有什么现有的成果,免得做许多无用功. Linux Mailing List就是一个不错的地方,这里聚集着很多提 ...

  2. 我的4g网卡运行着GNU/Linux -- 某4g无线网卡的逆向工程与主线Linux移植 (二)

    修改lk2nd 观察lk2nd现有的设备树,和一部分实现,发现lk2nd的设备树其实也并不是只用于原装bootloader识别,大佬们也实现了设备树中的按键定义在系统中的注册. 查看littleker ...

  3. 我的4g网卡运行着GNU/Linux -- 某4g无线网卡的逆向工程与主线Linux移植 (一)

    最近,某鱼和某宝上开始出现一些基于高通处理器的4g无线网卡,有一些网卡采用的方案也就是我之前折腾过的红米2的主控msm8916.如果能够在这些无线网卡上跑起主线linux,这应该是世界上最便宜的arm ...

  4. 中招!330 万台老年机被植木马,背后黑幕细思极恐

    整理 | 王晓曼 出品 | 程序人生 (ID:coder _life) 近日,一条以侵犯老年人合法权益为犯罪手段的黑灰产业链70余名涉案人员被浙江省绍兴市新昌县法院判处刑罚. 这条黑灰产业链是以非法获 ...

  5. 老年机打不出电话拨号失败服务器无响应,老年机为什么打不出去电话

    大家好,我是时间财富网智能客服时间君,上述问题将由我为大家进行解答. 若老年机打不出去电话,具体解决方法如下: 1.查看手机是否处于飞行模式: 2.检查手机信号是否稳定,若网络信号不好或无信号,请将手 ...

  6. 中国联通关闭2g 3g信号服务器,联通关闭2G3G信号 关闭2G信号老年机无法使用

    大多数乡村老人都是使用2G.3G信号的,现在农村的4G网络普及率并不高,所以,村民只能2G.3G信号.现在联通关闭2G3G信号了,由于自己的老年机无法打通,就了咨询了联通客服,2G.3G信号关闭了,老 ...

  7. [置顶] 记一个应届生的求职旅途

    记一个应届生的求职旅途 见有些同学发状态说大学期间要1.过四级2.拿到会计证,然后说了句毕业找工作去联通电信..我就纳闷了,有没有点常识,1.过四级是大学生的最低标准,很多企业单位根本跟英语没半毛钱关 ...

  8. 老年机按键串号_手机拨号键盘上的*号和#号到底有什么用?一般人只知道前三个...

    手机拨号键盘上的*号和#号到底有什么用?一般人只知道前三个. *号和#号由来 *号和#号的由来要从电话的发明说起,键盘上除了0-9十个数字,还配有*号和#号,构成3×4的阵列,这种排列方式比较工整,如 ...

  9. 记一个自认为写得有点复杂的sql语句

    记一个自认为写得有点复杂的sql语句,含义是跨3张表的select: select table_name,column_name,data_type,data_length,data_scale fr ...

  10. jpa query oracle 参数int为空_撸一个预言机(Oracle)服务,真香!—中篇

    本文作者:六天 一.文章结构 本文将通过上.中.下三篇文章带领大家一步步开发实现一个中心化的 Oracle 服务,并通过一个抽奖合约演示如何使用我们的 Oracle 服务.文章内容安排如下: 上篇:O ...

最新文章

  1. 送福利 | 送书5本《ASP.NET Core项目开发实战入门》带你走进ASP.NET Core开发
  2. php 异常错误信息用处,关于PHP中异常错误的处理详细介绍
  3. 论文浅尝 | 远程监督关系抽取的生成式对抗训练
  4. 详解7类Python运算符及代码举例
  5. java channelpipeline,Netty那点事(三)Channel与Pipeline
  6. 「理解HTTP」之常见的状态码
  7. c语言学习-字符串输出
  8. k8s与监控--从telegraf改造谈golang多协程精确控制
  9. 【luogu1337】【JSOI2004】平衡点 / 吊打XXX(模拟退火)
  10. [控件] 将字符串转换成贝塞尔曲线并执行动画
  11. 将给定的字符串划分为所有可能的IP地址 Restore IP Addresses
  12. Logstash自定义grok正则匹配规则
  13. 【electron】nsis重编译,自定义nsis校验弹窗警告的文案
  14. 自动控制原理学习--奈奎斯特稳定判据
  15. android 动画编辑器,开机动画编辑器 BootanimationEditor
  16. ENVI5.1 进行监督分类流程化工具时(classification workflow)界面显示不全的问题解决办法
  17. 零成本建立医学数据库之实践
  18. ggplot2绘制地图
  19. Arithmetic circuit
  20. 名帖47 钟繇 小楷《宣示表》

热门文章

  1. CSDN博客添加友情链接
  2. Linux下支持的视频文件格式,linux下视频格式转换与视频合并
  3. 为什么新建文本文档没有.txt后缀
  4. 使用ppo强化学习算法预测双色球彩票程序
  5. Android Binder实战开发指南之开篇
  6. ranger文件管理器修改默认文本编辑器为vim
  7. jmeter压力测试
  8. android安卓源码海量项目合集打包-1
  9. c语言实现《学生管理系统》
  10. Kotlin 文档入门-函数 集合