吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 339|回复: 7
上一主题 下一主题
收起左侧

[原创] SQL Assistant 14 注册码算法分析

  [复制链接]
跳转到指定楼层
楼主
lilongjiang 发表于 2026-11-9 17:12 回帖奖励

SQL Assistant 14 注册码算法分析:从汇编定位到 C++ 注册机实现

本文以 SQL Assistant 14.1.127 的 32 位 SqlAssistPrefs.dll 为样本,说明如何找到注册码校验函数、如何从汇编还原计算规则,以及如何用 C++ 生成满足这些规则的注册码。

下面的虚拟地址以首选加载基址 0x08100000 为准。本文完成的是静态还原和生成器自测,尚未在原软件中提交验证,因此不把自测通过等同于实际激活成功。

一、从注册窗口找到算法入口

1. 搜索控件和事件名称

先在程序文件中搜索注册窗口相关字符串。在 SqlAssistPrefs.dll 中,可以找到:

edRegName
edRegCompany
edRegSn
btnRegOK
btnRegOKClick
Invalid License Key!

edRegSn 是注册码输入框的名称,btnRegOKClick 是注册按钮事件名称。程序保留了 Delphi 的部分窗体与方法元数据,因此可以从名称继续找到代码地址。

注意名称可能出现在窗体资源、类型信息和方法表中。窗体资源里的名称只说明事件绑定关系;真正要找的是方法表中与名称相邻的函数指针。

在文件偏移 0x014FA560 处,有下面这条方法记录:

14 00 | 78 E8 60 09 | 0D | 62 74 6E 52 65 67 4F 4B 43 6C 69 63 6B
长度     方法地址     名称长度            btnRegOKClick

按小端序解释,78 E8 60 09 为 0x0960E878。转到这个虚拟地址并检查代码,就找到了注册按钮处理函数。

文件偏移和虚拟地址不能混用。手工换算时,先找到所属 PE 节,再使用:

VA = ImageBase + 节的 VirtualAddress + (文件偏移 - 节的 PointerToRawData)

2. 跟进决定成功与失败的调用

按钮处理函数中的关键指令:

0960E89A  lea  edx, [ebp-8]
0960E89D  mov  eax, [ebx+454h]   ; 注册码输入框对象
0960E8A3  call 082FFB10h         ; 读取输入框文字
0960E8A8  mov  eax, [ebp-8]
0960E8AB  lea  edx, [ebp-4]
0960E8AE  call 0844CA38h         ; 去除首尾空白
0960E8B3  mov  eax, [ebp-4]
0960E8B6  call 0880770Ch         ; 校验注册码
0960E8BB  test eax, eax
0960E8BD  jne  0960EAAFh         ; 非零进入成功分支

最值得跟进的是 test eax,eax 前面的调用。它的返回值直接决定成功与失败,因此 0x0880770C 是校验入口。

检查成功分支还能发现,姓名和公司是在校验返回非零之后读取的。在这条调用路径中,注册码校验函数只接收注册码,这两个字段不参与计算。

沿 0x0880770C 继续分析,会遇到 0x0880743C。前者检查外层格式与校验和,后者检查核心字段:

注册按钮 → 读取并 Trim → 外层校验 → 核心校验 → 返回类型代码

DLL 中出现 RSA、AES 等名称并不能证明注册码使用它们,判断应以这条调用路径实际执行的指令为依据。

二、外层校验:先还原字符串布局

下面的字符位置从 1 开始,C++ 下标从 0 开始。原程序使用 UTF-16 字符串,每字符占两字节;本文生成的字符都属于 ASCII,可以用 std::string 按字节重写。

1. 长度和固定标记

08807739  mov  eax, [ebp-4]
0880773C  call 0810CDB8h         ; 字符串长度
08807741  cmp  eax, 28h          ; 0x28 = 40
08807744  jne  088078DEh         ; 不符合则失败

0880774E  mov  ecx, 4           ; 取 4 个字符
08807753  mov  edx, 0Bh         ; 从第 11 个字符开始
08807758  mov  eax, [ebp-4]
0880775B  call 0810DE08h         ; 子串操作
08807760  mov  eax, [ebp-8]
08807763  mov  edx, 08807918h    ; UTF-16 字符串 "-SAN"
08807768  call 0810DDD0h         ; 字符串比较
0880776D  jne  088078DEh

条件就是:长度为 40,第 11~14 字符必须为 -SAN。对应 C++:

if (key.size() != 40 || key.substr(10, 4) != "-SAN") {
    return 0;
}

0810CDB8h、0810DE08h 的用途需要结合函数内部实现和参数确认。这里可通过返回字符串长度、复制指定范围的 UTF-16 字符等行为识别。

2. 删除外壳字符

接下来的子串操作先取原串第 3 个字符至末尾,再拼接剩余串的第 1~12 个字符和第 14~38 个字符,得到 37 字符:

std::string body = key.substr(2);
std::string text = body.substr(0, 12) + body.substr(13);

这一步要跟清楚拼接函数的参数顺序。某个子串先被计算出来,不代表它最终排在左边。

3. 识别字符交换

下面的循环调用 0x088076C4:

088077CD  mov  ebx, 1
088077D2  push ebp
088077D3  mov  edx, 9
088077D8  sub  edx, ebx
088077DA  mov  eax, ebx
088077DC  call 088076C4h
088077E1  pop  ecx
088077E2  inc  ebx
088077E3  cmp  ebx, 5
088077E6  jne  088077D2h

跟进被调函数,可以看到它读取两个位置的 word 再交叉写回,因此是字符交换。循环中 EAX=i、EDX=9-i,i 从 1 到 4,得到:

(1,8)、(2,7)、(3,6)、(4,5)

随后同一函数还被调用两次,参数为 (1,36)、(2,37)。接着出现:

0880780A  lea  eax, [ebp-4]
0880780D  call 0810CE8Ch
08807812  mov  word ptr [eax+16h], 39h

0x39 是字符 9,0x16 为 22 字节,对应 UTF-16 下标 11,即第 12 个字符。它被强制写成 9。

完整变换为:

for (unsigned i = 1; i <= 4; ++i) {
    std::swap(text[i - 1], text[8 - i]);
}
std::swap(text[0], text[35]);
std::swap(text[1], text[36]);
text[11] = '9';

三、外层校验和怎么算

1. 从调用参数确定范围

第一次校验和调用:

08807819  push 1Ah             ; 第二段终点:26
0880781B  mov  ecx, 14h        ; 第二段起点:20
08807820  mov  edx, 12h        ; 第一段终点:18
08807825  mov  eax, 0Ch        ; 第一段起点:12
0880782A  call 0880766Ch

第二次传入 EAX=28、EDX=31、ECX=33,栈上传入 37。结合被调函数里的两个遍历循环,得到:

结果保存位置 参与求和的位置,包含两端
第 1~4 字符 第 12~18、20~26 字符
第 5~8 字符 第 28~31、33~37 字符

2. 从指令还原公式

函数 0x0880766C 的两个循环都把字符码累加到 BX,尾部为:

088076AA  movzx eax, bx
088076AD  and   eax, 7
088076B0  imul  ax, ax, 3E8h    ; 0x3E8 = 1000
088076B5  add   bx, ax
088076B8  mov   eax, ebx

调用者随后取 AX,所以有效结果为 16 位。设字符码之和为 s:

s = 字符码之和 mod 65536
checksum = (s + (s & 7) × 1000) mod 65536

结果格式化为四位十六进制数,与字符串中的字段进行不区分大小写的比较。ASCII 范围的实现为:

unsigned checksum(const std::string& s) {
    unsigned total = 0;
    for (unsigned char ch : s) {
        total = (total + ch) & 0xffffu;
    }
    return (total + (total & 7u) * 1000u) & 0xffffu;
}

通过两组检查后,外层取第 12~36 个字符,进入 0x0880743C。

四、核心校验:25 字符里的整数关系

1. 根据长度与分隔符拆字段

0880744C  call 0810CDB8h
08807451  cmp  eax, 19h                 ; 长度 25
08807454  jne  088075D6h
0880745A  cmp  word ptr [ebx+0Eh], 2Dh  ; 第 8 字符为 '-'
08807465  cmp  word ptr [ebx+1Eh], 2Dh  ; 第 16 字符为 '-'
08807470  cmp  word ptr [ebx+28h], 2Dh  ; 第 21 字符为 '-'

上面省略了分隔符比较之间相同的失败跳转。结合后续读取位置,可以拆出:

ddddddd-ccccccc-TMxx-aabb
字符位置 字段 含义
1~7 ddddddd 序列字段,经外层处理后首位为 9
9~15 ccccccc 七字符编码的整数
17 T 类型,允许 1/E/P/S
18 M 模式占位符,实际允许 L/M
19~20 xx 核心函数未直接解释,生成时取 00
22~23 aa 序列字段的数位校验和
24~25 bb 编码整数的数位校验和

类型与模式的允许范围来自两个字符集合检查;这里不推定它们的商业授权含义。xx 虽然没有在核心函数中被直接解释,仍参与外层校验和。

2. 数位校验和

程序解析前七位十进制数后,调用 0x0880740C:

0880740E  mov  ebx, eax
08807410  mov  esi, 16h       ; 初值 22
; 省略零值判断
08807419  mov  eax, ebx
0880741B  mov  ecx, 0Ah
08807420  xor  edx, edx
08807422  div  ecx            ; EAX=商,EDX=余数
08807424  add  esi, edx       ; 累加末位数字
08807426  mov  ecx, 0Ah
0880742B  mov  eax, ebx
0880742D  xor  edx, edx
0880742F  div  ecx
08807431  mov  ebx, eax       ; 去掉末位
08807433  test ebx, ebx
08807435  ja   08807419h
08807437  mov  eax, esi

公式为:

digitSum(n) = 22 + n 的十进制各位数字之和

程序用它检查 aa 和 bb:前者对应序列字段,后者对应编码字段解出的整数。它与上一节的字符码校验和是两个不同函数。

3. 自定义字符表与 32 进制位权

解码循环的关键部分:

088074E8  xor   edi, edi            ; 累加结果
088074EA  mov   ebp, 1              ; 初始位权
088074EF  mov   esi, 0Fh            ; 从第 15 字符向前遍历
088074F4  movzx eax, word ptr [ebx+esi*2-2]
088074F9  call  0844B5A4h           ; 转小写
088074FE  mov   edx, eax
08807500  mov   ecx, 1
08807505  mov   eax, 0880762Ch       ; 字符表
0880750A  call  0844C85Ch           ; 查字符位置,从 1 起算
0880750F  imul  ebp                 ; 字符值乘位权
08807511  add   edi, eax
08807513  shl   ebp, 5              ; 位权乘 32
08807516  dec   esi
08807517  cmp   esi, 8
0880751A  jne   088074F4h

0x0880762C 处的字符表为:

123456789abcdefghikjlmnopqrstuv

只有 31 个字符,位置对应数值 1~31,查询不到时返回 0。中间的顺序是 ikjl,不能替换成通常的字母顺序。生成器用不在表中的字符 0 表示零值。

原汇编从右往左计算位权和;C++ 可以等价地从左往右计算:

value = value × 32 + 当前字符值

运算按 32 位取模,C++ 用 uint32_t 保存结果。不能依赖有符号整数溢出实现这个行为。

4. 决定有效性的等式

解码结果保存在 EDI,随后执行:

08807544  lea  eax, [ebx+0Ah]       ; 从第 6 字符取两位十进制数
08807547  xor  ecx, ecx
08807549  mov  edx, 2
0880754E  call 0844C498h
08807553  imul edx, [esp+4], 186A0h ; aa × 100000
0880755B  sub  edi, edx
0880755D  sub  edi, eax             ; 减去序列字段末两位
0880755F  cmp  word ptr [ebx+22h], 4Dh ; 模式是否为 'M'
08807564  sete al
08807567  mov  ecx, 10h
0880756C  mov  edx, 1
08807571  call 0844E130h            ; M 取 1,否则取 16
08807576  mov  esi, eax
08807578  mov  eax, 80000000h
0880757D  sub  eax, esi
0880757F  cmp  edi, eax
08807581  je   0880758Eh
08807583  mov  eax, 0C0000000h
08807588  sub  eax, esi
0880758A  cmp  edi, eax
0880758C  jne  088075D6h

设 serial 为序列字段,value 为编码值;模式为 M 时 delta=1,模式为 L 时 delta=16。必须满足:

value - aa × 100000 - serial % 100
    = family - delta                       (按 32 位取模)

family ∈ {0x80000000, 0xC0000000}
aa = digitSum(serial)
bb = digitSum(value)

通过后,类型字符 1/P/S/E 分别返回 1/2/3/4;模式为 M 时再置上 0x100 标志位。零表示失败。

五、反向计算一组注册码

把核心等式移项:

value = family - delta + digitSum(serial) × 100000 + serial % 100

因此不需要枚举注册码。选择序列字段、类型、模式和常量组,计算整数,再按字符表编码即可。

取下面这组参数:

serial = 9123456
类型   = P
模式   = L
family = 0x80000000

计算过程:

aa = 22 + 9 + 1 + 2 + 3 + 4 + 5 + 6 = 52
delta = 16

value = 2147483648 - 16 + 52 × 100000 + 56
      = 2152683688
      = 0x804F58A8

bb = 22 + 2 + 1 + 5 + 2 + 6 + 8 + 3 + 6 + 8 + 8 = 71

编码时反复取最低五位 value & 31,查字符表,再右移五位,从右往左填入七个位置,得到 204um58。用解码方向核算:

字符 数值 上一步 × 32 + 数值
2 2 2
0 0 64
4 4 2052
u 30 65694
m 22 2102230
5 5 67271365
8 8 2152683688

拼成核心串:

9123456-204um58-PL00-5271

再构造尚未换位的 37 字符外层串:

00000000-SA9123456-204um58-PL00-52710

前八个零预留给两组校验和,末尾的 0 是生成器选择的附加字符,参与第二组校验和。

第一组求和文本为 9123456204um58,字符码之和为 851:

851 & 7 = 3
851 + 3 × 1000 = 3851 = 0x0F0B

第二组为 PL0052710,字符码之和为 507:

507 & 7 = 3
507 + 3 × 1000 = 3507 = 0x0DB3

填入后得到:

0F0B0DB3-SA9123456-204um58-PL00-52710

然后撤销外层校验器的操作:

  1. 把第 12 字符从 9 恢复为 N,使最终标记正确。
  2. 逆序交换 (2,37)、(1,36)、(4,5)、(3,6)、(2,7)、(1,8)。
  3. 在换位后字符串的前 12 字符之后插入 -,补回被删除的位置。
  4. 在最前面补上两个字符,生成器选择 SA。

结果为:

SA3BD0B001-SAN-123456-204um58-PL00-5270F

原始前缀的两个字符不参与上述校验,使用 SA 是构造选择;补回的被删除位置也不直接参与计算。它们与必须匹配的 -SAN 标记需要区分。

六、完整 C++ 注册机实现

下面包含生成器、核心校验器、外层校验器和自测入口。保存为 keygen.cpp 即可编译,不需要其他源码文件。程序按 ASCII 注册码处理,覆盖本文生成的字符范围;它只生成和校验字符串,不自动提交注册。

#include <string>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <stdexcept>
#include <iostream>
#include <random>

static const std::string alphabet = "123456789abcdefghikjlmnopqrstuv";
static unsigned digitSum(uint32_t n) {
    unsigned sum = 22;
    do { sum += n % 10; n /= 10; } while (n);
    return sum;
}
static uint32_t decimal(const std::string& s, size_t count) {
    uint32_t n = 0;
    for (size_t i=0; i < count && i < s.size() && s[i]>='0' && s[i]<='9'; ++i)
        n = n * 10 + s[i] - '0';
    return n;
}
static unsigned checksum(const std::string& s) {
    unsigned n = 0;
    for (unsigned char c : s) n = (n + c) & 65535;
    return (n + (n & 7) * 1000) & 65535;
}
static char lower(char c) { return c >= 'A' && c <= 'Z' ? c+32 : c; }
static std::string hex4(unsigned n) {
    char buf[16]; sprintf_s(buf, "%04X", n); return buf;
}
static bool equalIgnoreCase(std::string a, std::string b) {
    std::transform(a.begin(), a.end(), a.begin(), lower);
    std::transform(b.begin(), b.end(), b.begin(), lower);
    return a == b;
}
static unsigned coreValidate(const std::string& c) {
    if (c.size()!=25 || c[7]!='-' || c[15]!='-' || c[20]!='-') return 0;
    if (std::string("1EPS").find(c[16])==std::string::npos ||
        std::string("LM").find(c[17])==std::string::npos) return 0;
    uint32_t serial = decimal(c,7), first = decimal(c.substr(21),2), value=0;
    if (!serial || digitSum(serial)!=first) return 0;
    for (size_t i=8;i<15;++i) {
        size_t p = alphabet.find(lower(c[i]));
        value = (value<<5) + (p==std::string::npos ? 0 : static_cast<unsigned>(p)+1);
    }
    if (!value || digitSum(value)!=decimal(c.substr(23),2)) return 0;
    uint32_t delta = c[17]=='M' ? 1 : 16;
    uint32_t decoded = value - first * 100000 - decimal(c.substr(5),2);
    if (decoded != 0x80000000u-delta && decoded != 0xc0000000u-delta) return 0;
    unsigned type = c[16]=='P' ? 2 : c[16]=='S' ? 3 : c[16]=='E' ? 4 : 1;
    return type | (c[17]=='M' ? 0x100 : 0);
}
static void swapAt(std::string& s, unsigned a, unsigned b) { std::swap(s[a-1],s[b-1]); }
static unsigned validate(std::string key) {
    while (!key.empty() && static_cast<unsigned char>(key.front())<=32) key.erase(0,1);
    while (!key.empty() && static_cast<unsigned char>(key.back())<=32) key.pop_back();
    if (key.size()!=40 || key.substr(10,4)!="-SAN") return 0;
    std::string body=key.substr(2), t=body.substr(0,12)+body.substr(13);
    for (unsigned i=1;i<=4;++i) swapAt(t,i,9-i);
    swapAt(t,1,36); swapAt(t,2,37); t[11]='9';
    if (!equalIgnoreCase(t.substr(0,4),hex4(checksum(t.substr(11,7)+t.substr(19,7))))) return 0;
    if (!equalIgnoreCase(t.substr(4,4),hex4(checksum(t.substr(27,4)+t.substr(32,5))))) return 0;
    return coreValidate(t.substr(11,25));
}
static std::string generate(unsigned serial, char edition, char mode, uint32_t family) {
    if (serial<9000000 || serial>9999999) throw std::runtime_error("serial must be 9000000..9999999");
    if (std::string("1EPS").find(edition)==std::string::npos || std::string("LM").find(mode)==std::string::npos)
        throw std::runtime_error("unsupported edition or mode");
    if (family!=0x80000000u && family!=0xc0000000u) throw std::runtime_error("unsupported family");
    unsigned a=digitSum(serial);
    uint32_t value=family-(mode=='M'?1:16)+a*100000+serial%100, v=value;
    std::string encoded(7,'0');
    for (int i=6;i>=0;--i) { unsigned d=v&31; encoded[i]=d?alphabet[d-1]:'0'; v>>=5; }
    char buf[80]; sprintf_s(buf,"%07u-%s-%c%c00-%02u%02u",serial,encoded.c_str(),edition,mode,a,digitSum(value));
    std::string core=buf;
    if (!coreValidate(core)) throw std::runtime_error("unrepresentable checksum");
    std::string t="00000000-SA"+core+"0";
    t.replace(0,4,hex4(checksum(t.substr(11,7)+t.substr(19,7))));
    t.replace(4,4,hex4(checksum(t.substr(27,4)+t.substr(32,5))));
    t[11]='N'; swapAt(t,2,37); swapAt(t,1,36);
    for (unsigned i=4;i>0;--i) swapAt(t,i,9-i);
    std::string key="SA"+t.substr(0,12)+"-"+t.substr(12);
    if (validate(key)!=coreValidate(core)) throw std::runtime_error("internal validation failed");
    return key;
}

static void selfTest() {
    std::mt19937 rng(127);
    unsigned count = 0;
    if (generate(9123456, 'P', 'L', 0x80000000u) !=
        "SA3BD0B001-SAN-123456-204um58-PL00-5270F")
        throw std::runtime_error("fixed vector mismatch");
    for (char edition : std::string("1EPS")) {
        for (char mode : std::string("LM")) {
            for (uint32_t family : {0x80000000u, 0xc0000000u}) {
                for (unsigned i = 0; i < 100; ++i) {
                    unsigned serial = 9000000u + rng() % 1000000u;
                    const std::string key = generate(serial, edition, mode, family);
                    unsigned type = edition == 'P' ? 2u : edition == 'S' ? 3u
                                  : edition == 'E' ? 4u : 1u;
                    unsigned expected = type | (mode == 'M' ? 0x100u : 0u);
                    if (validate(key) != expected)
                        throw std::runtime_error("generated key rejected");
                    std::string damaged = key;
                    damaged[10] = 'X';
                    if (validate(damaged) != 0 || validate(key.substr(0, 39)) != 0)
                        throw std::runtime_error("malformed key accepted");
                    ++count;
                }
            }
        }
    }
    std::cout << count << " C++ generation/validation cases passed.\n";
}

static unsigned parseUnsigned(const char* arg) {
    std::string s(arg);
    if (s.empty() || s.front() == '-') throw std::runtime_error("invalid unsigned integer");
    size_t end = 0;
    unsigned long long value = std::stoull(s, &end, 0);
    if (end != s.size() || value > 0xffffffffull)
        throw std::runtime_error("invalid unsigned integer");
    return static_cast<unsigned>(value);
}

int main(int argc, char** argv) {
    try {
        if (argc == 2 && std::string(argv[1]) == "--self-test") {
            selfTest();
            return 0;
        }
        if (argc == 3 && std::string(argv[1]) == "--validate") {
            unsigned type = validate(argv[2]);
            std::cout << "type_code=" << type << '\n';
            return type ? 0 : 1;
        }
        unsigned serial = 9123456;
        char edition = 'P', mode = 'L';
        uint32_t family = 0x80000000u;
        if (argc != 1) {
            if (argc != 6 || std::string(argv[1]) != "--generate" ||
                std::string(argv[3]).size() != 1 || std::string(argv[4]).size() != 1)
                throw std::runtime_error("Usage: Keygen.exe [--generate serial edition mode family | --validate key | --self-test]");
            serial = parseUnsigned(argv[2]);
            edition = argv[3][0];
            mode = argv[4][0];
            family = parseUnsigned(argv[5]);
        }
        const std::string key = generate(serial, edition, mode, family);
        std::cout << key << '\n' << "type_code=" << validate(key) << '\n';
        return 0;
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
        return 2;
    }
}

七、编译与运行

在 Visual Studio 的 x64 Native Tools Command Prompt 中进入源码所在目录:

cl /nologo /std:c++17 /utf-8 /EHsc /O2 /W4 /MT keygen.cpp /Fe:Keygen.exe

默认运行:

Keygen.exe

输出:

SA3BD0B001-SAN-123456-204um58-PL00-5270F
type_code=2

指定全部参数:

Keygen.exe --generate 9123456 P L 0x80000000

参数依次为序列字段、类型、模式和常量组。序列字段取 9000000~9999999;类型为 1/E/P/S,模式为 L/M,常量组为 0x80000000 或 0xC0000000。

校验已有字符串:

Keygen.exe --validate "SA3BD0B001-SAN-123456-204um58-PL00-5270F"

类型代码为零表示未通过还原校验器,非零表示通过;模式为 M 时还包含 0x100 标志位。

运行自测:

Keygen.exe --self-test

自测使用固定随机种子,遍历 4 种类型、2 种模式、2 组常量,每个组合取 100 个序列字段,共 1600 组。同时检查固定算例、生成后的类型代码,以及损坏标记和截断长度时是否被拒绝。已完成的自测输出:

1600 C++ generation/validation cases passed.

这证明生成器与还原校验器内部自洽。原始 DLL 对相同输入的返回结果,以及原软件是否还有其他授权限制,仍需另外验证。


注册机界面


LicenseTool.zip (97.22 KB, 下载次数: 38)

免费评分

参与人数 1吾爱币 +2 热心值 +1 收起 理由
Patches + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
rwxqaz098 发表于 2026-11-10 11:10
牛逼克拉斯 分析到位
3#
baysery 发表于 2026-11-10 11:11
4#
xcl 发表于 2026-11-10 13:31
太强了!大佬,有一个问题,有ai了还用学习以前的知识么?
5#
 楼主| lilongjiang 发表于 2026-11-10 14:07 |楼主
xcl 发表于 2026-11-10 13:31
太强了!大佬,有一个问题,有ai了还用学习以前的知识么?

不用啊,哪里不懂就让AI帮你分析,就像你要去一个地方以前走着去,现在直接叫个出租车,反正能到达目的就行了,前提你要懂个大概,细节可以让AI补充
6#
jway 发表于 2026-11-10 14:52
牛人太强了,2022年以前一直用SQL Assistant v11.5.362.0(这是我最后用的版本),后来改用SQL Prompt,就是因为前者没有注册码,对新版的SSMS不支持,无奈就换成了后者。
7#
jway 发表于 2026-11-10 14:58
再补充一句,SQL Assistant的BEGIN...END 块跳转,特别好用!
8#
xzchina 发表于 2026-11-10 19:35
很少用这些工具了, 现在都用AI了.
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-11-11 07:16

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表