Wine

Wine makes it possible to run Windows programs alongside any Unix-like operating system, particularly Linux. At its heart, Wine is an implementation of the Windows Application Programing Interface (API) library, acting as a bridge between the Windows program and Linux. Think of Wine as a compatibility layer, when a Windows program tries to perform a function that Linux doesn't normally understand, Wine will translate that program's instruction into one supported by the system. For example, if a program asks the system to create a Windows pushbutton or text-edit field, Wine will convert that instruction into its Linux equivalent in the form of a command to the window manager using the standard X11 protocol.

If you have access to the Windows program source code, Wine can also be used to recompile a program into a format that Linux can understand more easily. Wine is still needed to launch the program in its recompiled form, however there are many advantages to compiling a Windows program natively within Linux. For more information, see the Winelib User Guide.

You can simply invoke the wine command to get a small help message:

Usage: wine PROGRAM [ARGUMENTS...]   Run the specified program
       wine --help                   Display this help and exit
       wine --version                Output version information and exit

The first argument should be the name of the file you want wine to execute.  Most binary Wine packages will associate Wine with .exe files for you. If that is the case, you should be able to simply double-click on the .exe file in your file manager, just like in Windows. You can also right-click on the file, choose "Run with", and choose "Wine". 

Windows的.exe和.dll是PE (Portable Executable)格式,Linux的是ELF(Executable and Linkable Format)格式。可执行程序wine是ELF格式,wine去执行的notepad.exe是PE格式。

wine的源码里有多个名为*loader.c的,还有个pe.c:

复制代码
 *	PE dumping utility
    switch (mach)
    {
    case IMAGE_FILE_MACHINE_UNKNOWN:	return "Unknown";
    case IMAGE_FILE_MACHINE_I860:	return "i860";
    case IMAGE_FILE_MACHINE_I386:	return "i386";
    case IMAGE_FILE_MACHINE_R3000:	return "R3000";
    case IMAGE_FILE_MACHINE_R4000:	return "R4000";
    case IMAGE_FILE_MACHINE_R10000:	return "R10000";
    case IMAGE_FILE_MACHINE_ALPHA:	return "Alpha";
    case IMAGE_FILE_MACHINE_POWERPC:	return "PowerPC";
    case IMAGE_FILE_MACHINE_AMD64:      return "AMD64";
    case IMAGE_FILE_MACHINE_IA64:       return "IA64";
    case IMAGE_FILE_MACHINE_ARM64:      return "ARM64";
    case IMAGE_FILE_MACHINE_ARM:        return "ARM";
    case IMAGE_FILE_MACHINE_ARMNT:      return "ARMNT";
    case IMAGE_FILE_MACHINE_THUMB:      return "ARM Thumb";
    }
复制代码

The Mips R10000 is a dynamic, superscalar microprocessor that implements the 64-bit Mips 4 instruction set architecture. It fetches and decodes four instructions per cycle and dynamically issues them to five fully-pipelined, low-latency execution units. Instructions can be fetched and executed speculatively beyond branches. Quite possibly the world's fastest CPU when released, code-named the "T5" after Terminator 2 which featured graphics designed on Silicon Graphics systems, the MIPS R10000 (Do you sense the play on T-1000 ?) was released in late 1995 and was first avaliable on SGI's Indigo2 systems. 这个叫“终结者”。HP和Intel搞了EPIC(史诗), EPYC读音和EPIC一样吧?中国文化认为“名贱好养活” :-)

AMD Server 'EPYC' CPU Market Share. Starting with the server-side, AMD has reported a share of 8%, almost double compared to 2018. The stats from Mercury Research highlighted a market share of 4.5% till Q4 2019 so this is where we see a large desperate between what research firms are claiming and what AMD's own internal estimates are showing. [link]

名字太长不像是好主意:The Z8000 / Z80,000 / Z16C00 CPU homepage (kranenborg.org)

The R4000 is a microprocessor developed by MIPS Computer Systems that implements the MIPS III instruction set architecture. Officially announced on 1 October 1991, it was one of the first 64-bit microprocessors and the first MIPS III implementation. In the early 1990s, when RISC microprocessors were expected to replace CISC microprocessors such as the Intel i486, the R4000 was selected to be the microprocessor of the Advanced Computing Environment, an industry standard that intended to define a common RISC platform. ACE ultimately failed for a number of reasons, but the R4000 found success in the workstation and server markets.

The frequently asked questions about BogoMips (tldp.org) As a very approximate guide, the BogoMips can be calculated by:

复制代码
System                      BogoMips           Comparison
Intel 8088                  clock * 0.004         0.02
Intel/AMD 386SX             clock * 0.14          0.8
Intel/AMD 386DX             clock * 0.18          1 (definition)
Motorola 68030              clock * 0.25          1.4
Cyrix/IBM 486               clock * 0.34          1.8
Intel Pentium               clock * 0.40          2.2
Intel 486                   clock * 0.50          2.8
AMD 5x86                    clock * 0.50          2.8
Mips R4000/R4400            clock * 0.50          2.8
Motorola 68040              clock * 0.67          3.7
PowerPC 603                 clock * 0.67          3.7
PowerPC 601                 clock * 0.84          4.7
Alpha 21064/21064A          clock * 0.99          5.5
Alpha 21066/21066A          clock * 0.99          5.5
Alpha 21164/21164A          clock * 0.99          5.5
Intel Pentium Pro           clock * 0.99          5.5
Cyrix 5x86/6x86             clock * 1.00          5.6
Intel Pentium II/III        clock * 1.00          5.6
AMD K7/Athlon               clock * 1.00          5.6
Intel Celeron               clock * 1.00          5.6
Intel Itanium               clock * 1.00          5.6
Mips R4600                  clock * 1.00          5.6
Intel Itanium 2             clock * 1.49          8.3
Alpha 21264                 clock * 1.99         11.1
Centaur VIA                 clock * 1.99         11.1
AMD K5/K6/K6-2/K6-III       clock * 2.00         11.1
AMD Athlon XP/Athlon 64     clock * 2.00         11.1
AMD Duron/Opteron           clock * 2.00         11.1
UltraSparc II               clock * 2.00         11.1
Pentium MMX                 clock * 2.00         11.1
复制代码

Wine will not allow running native Windows drivers under Unix. This comes mainly because (look at the generic architecture schemas) Wine doesn't implement the kernel features of Windows (kernel here really means the kernel, not the KERNEL32 DLL), but rather sets up a proxy layer on top of the Unix kernel to provide the NTDLL and KERNEL32 features. This means that Wine doesn't provide the inner infrastructure to run native drivers, either from the Win9x family or from the NT family.

In other words, Wine will only be able to provide access to a specific device, if and only if, 1/ this device is supported in Unix (there is Unix-driver to talk to it), 2/ Wine has implemented the proxy code to make the glue between the API of a Windows driver, and the Unix interface of the Unix driver.

Wine, however, tries to implement in the various DLLs needing to access devices to do it through the standard Windows APIs for device drivers in user space. This is for example the case for the multimedia drivers, where Wine loads Wine builtin DLLs to talk to the OSS interface, or the ALSA interface. Those DLLs implement the same interface as any user space audio driver in Windows.

dlls/ntdll/unix/loader.c里"Usage: wine PROGRAM [ARGUMENTS...]   Run the specified program\n",

复制代码
NTSTATUS exec_wineloader( char **argv, int socketfd, const pe_image_info_t *pe_info )
{
    int is_child_64bit = (pe_info->cpu == CPU_x86_64 || pe_info->cpu == CPU_ARM64);
    ULONGLONG res_start = pe_info->base;
    ULONGLONG res_end = pe_info->base + pe_info->map_size;
    const char *loader = argv0;
    const char *loader_env = getenv( "WINELOADER" );
    char preloader_reserve[64], socket_env[64];

    if (!is_win64 ^ !is_child_64bit)
    {
        /* remap WINELOADER to the alternate 32/64-bit version if necessary */
        if (loader_env)
        {
            int len = strlen( loader_env );
            char *env = malloc( sizeof("WINELOADER=") + len + 2 );

            if (!env) return STATUS_NO_MEMORY;
            strcpy( env, "WINELOADER=" );
            strcat( env, loader_env );
            if (is_child_64bit)
            {
                strcat( env, "64" );
            }
            else
            {
                len += sizeof("WINELOADER=") - 1;
                if (!strcmp( env + len - 2, "64" )) env[len - 2] = 0;
            }
            loader = env;
            putenv( env );
        }
        else loader = is_child_64bit ? "wine64" : "wine";
    }

    signal( SIGPIPE, SIG_DFL );

    sprintf( socket_env, "WINESERVERSOCKET=%u", socketfd );
    sprintf( preloader_reserve, "WINEPRELOADRESERVE=%x%08x-%x%08x",
             (ULONG)(res_start >> 32), (ULONG)res_start, (ULONG)(res_end >> 32), (ULONG)res_end );

    putenv( preloader_reserve );
    putenv( socket_env );

    return loader_exec( loader, argv, pe_info->cpu );
}

static NTSTATUS map_so_dll( const IMAGE_NT_HEADERS *nt_descr, HMODULE module )
{
    static const char builtin_signature[32] = "Wine builtin DLL";
    IMAGE_DATA_DIRECTORY *dir;
    IMAGE_DOS_HEADER *dos;
    IMAGE_NT_HEADERS *nt;
    IMAGE_SECTION_HEADER *sec;
    BYTE *addr = (BYTE *)module;
    DWORD code_start, code_end, data_start, data_end, align_mask;
    int delta, nb_sections = 2;  /* code + data */
    unsigned int i;
    DWORD size = (sizeof(IMAGE_DOS_HEADER)
                  + sizeof(builtin_signature)
                  + sizeof(IMAGE_NT_HEADERS)
                  + nb_sections * sizeof(IMAGE_SECTION_HEADER));

    if (anon_mmap_fixed( addr, size, PROT_READ | PROT_WRITE, 0 ) != addr) return STATUS_NO_MEMORY;

    dos = (IMAGE_DOS_HEADER *)addr;
    nt  = (IMAGE_NT_HEADERS *)((BYTE *)(dos + 1) + sizeof(builtin_signature));
    sec = (IMAGE_SECTION_HEADER *)(nt + 1);

    /* build the DOS and NT headers */

    dos->e_magic    = IMAGE_DOS_SIGNATURE;
    dos->e_cblp     = 0x90;
    dos->e_cp       = 3;
    dos->e_cparhdr  = (sizeof(*dos) + 0xf) / 0x10;
    dos->e_minalloc = 0;
    dos->e_maxalloc = 0xffff;
    dos->e_ss       = 0x0000;
    dos->e_sp       = 0x00b8;
    dos->e_lfanew   = sizeof(*dos) + sizeof(builtin_signature);

    *nt = *nt_descr;

    delta      = (const BYTE *)nt_descr - addr;
    align_mask = nt->OptionalHeader.SectionAlignment - 1;
    code_start = (size + align_mask) & ~align_mask;
    data_start = delta & ~align_mask;
#ifdef __APPLE__
    {
        Dl_info dli;
        unsigned long data_size;
        /* need the mach_header, not the PE header, to give to getsegmentdata(3) */
        dladdr(addr, &dli);
        code_end   = getsegmentdata(dli.dli_fbase, "__DATA", &data_size) - addr;
        data_end   = (code_end + data_size + align_mask) & ~align_mask;
    }
#else
    code_end   = data_start;
    data_end   = (nt->OptionalHeader.SizeOfImage + delta + align_mask) & ~align_mask;
#endif

    fixup_rva_ptrs( &nt->OptionalHeader.AddressOfEntryPoint, addr, 1 );

    nt->FileHeader.NumberOfSections                = nb_sections;
    nt->OptionalHeader.BaseOfCode                  = code_start;
#ifndef _WIN64
    nt->OptionalHeader.BaseOfData                  = data_start;
#endif
    nt->OptionalHeader.SizeOfCode                  = code_end - code_start;
    nt->OptionalHeader.SizeOfInitializedData       = data_end - data_start;
    nt->OptionalHeader.SizeOfUninitializedData     = 0;
    nt->OptionalHeader.SizeOfImage                 = data_end;
    nt->OptionalHeader.ImageBase                   = (ULONG_PTR)addr;

    /* build the code section */

    memcpy( sec->Name, ".text", sizeof(".text") );
    sec->SizeOfRawData = code_end - code_start;
    sec->Misc.VirtualSize = sec->SizeOfRawData;
    sec->VirtualAddress   = code_start;
    sec->PointerToRawData = code_start;
    sec->Characteristics  = (IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ);
    sec++;

    /* build the data section */

    memcpy( sec->Name, ".data", sizeof(".data") );
    sec->SizeOfRawData = data_end - data_start;
    sec->Misc.VirtualSize = sec->SizeOfRawData;
    sec->VirtualAddress   = data_start;
    sec->PointerToRawData = data_start;
    sec->Characteristics  = (IMAGE_SCN_CNT_INITIALIZED_DATA |
                             IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ);
    sec++;

    for (i = 0; i < nt->OptionalHeader.NumberOfRvaAndSizes; i++)
        fixup_rva_dwords( &nt->OptionalHeader.DataDirectory[i].VirtualAddress, delta, 1 );

    /* build the import directory */

    dir = &nt->OptionalHeader.DataDirectory[IMAGE_FILE_IMPORT_DIRECTORY];
    if (dir->Size)
    {
        IMAGE_IMPORT_DESCRIPTOR *imports = (IMAGE_IMPORT_DESCRIPTOR *)(addr + dir->VirtualAddress);

        while (imports->Name)
        {
            fixup_rva_dwords( &imports->u.OriginalFirstThunk, delta, 1 );
            fixup_rva_dwords( &imports->Name, delta, 1 );
            fixup_rva_dwords( &imports->FirstThunk, delta, 1 );
            if (imports->u.OriginalFirstThunk)
                fixup_rva_names( (UINT_PTR *)(addr + imports->u.OriginalFirstThunk), delta );
            if (imports->FirstThunk)
                fixup_rva_names( (UINT_PTR *)(addr + imports->FirstThunk), delta );
            imports++;
        }
    }

    /* build the resource directory */

    dir = &nt->OptionalHeader.DataDirectory[IMAGE_FILE_RESOURCE_DIRECTORY];
    if (dir->Size)
    {
        void *ptr = addr + dir->VirtualAddress;
        fixup_so_resources( ptr, ptr, delta );
    }

    /* build the export directory */

    dir = &nt->OptionalHeader.DataDirectory[IMAGE_FILE_EXPORT_DIRECTORY];
    if (dir->Size)
    {
        IMAGE_EXPORT_DIRECTORY *exports = (IMAGE_EXPORT_DIRECTORY *)(addr + dir->VirtualAddress);

        fixup_rva_dwords( &exports->Name, delta, 1 );
        fixup_rva_dwords( &exports->AddressOfFunctions, delta, 1 );
        fixup_rva_dwords( &exports->AddressOfNames, delta, 1 );
        fixup_rva_dwords( &exports->AddressOfNameOrdinals, delta, 1 );
        fixup_rva_dwords( (DWORD *)(addr + exports->AddressOfNames), delta, exports->NumberOfNames );
        fixup_rva_ptrs( addr + exports->AddressOfFunctions, addr, exports->NumberOfFunctions );
    }
    return STATUS_SUCCESS;
}

static ULONG_PTR find_pe_export( HMODULE module, const IMAGE_EXPORT_DIRECTORY *exports,
                                 const IMAGE_IMPORT_BY_NAME *name )
{
    const WORD *ordinals = (const WORD *)((BYTE *)module + exports->AddressOfNameOrdinals);
    const DWORD *names = (const DWORD *)((BYTE *)module + exports->AddressOfNames);

    if (name->Hint < exports->NumberOfNames)
    {
        char *ename = (char *)module + names[name->Hint];
        if (!strcmp( ename, (char *)name->Name ))
            return find_ordinal_export( module, exports, ordinals[name->Hint] );
    }
    return find_named_export( module, exports, (char *)name->Name );
}

static NTSTATUS fixup_ntdll_imports( const char *name, HMODULE module )
{
    const IMAGE_NT_HEADERS *nt;
    const IMAGE_IMPORT_DESCRIPTOR *descr;
    const IMAGE_DATA_DIRECTORY *dir;
    const IMAGE_THUNK_DATA *import_list;
    IMAGE_THUNK_DATA *thunk_list;

    nt = get_rva( module, ((IMAGE_DOS_HEADER *)module)->e_lfanew );
    dir = &nt->OptionalHeader.DataDirectory[IMAGE_FILE_IMPORT_DIRECTORY];
    if (!dir->VirtualAddress || !dir->Size) return STATUS_SUCCESS;

    descr = get_rva( module, dir->VirtualAddress );
    for (; descr->Name && descr->FirstThunk; descr++)
    {
        thunk_list = get_rva( module, descr->FirstThunk );

        /* ntdll must be the only import */
        if (strcmp( get_rva( module, descr->Name ), "ntdll.dll" ))
        {
            ERR( "module %s is importing %s\n", debugstr_a(name), (char *)get_rva( module, descr->Name ));
            return STATUS_PROCEDURE_NOT_FOUND;
        }
        if (descr->u.OriginalFirstThunk)
            import_list = get_rva( module, descr->u.OriginalFirstThunk );
        else
            import_list = thunk_list;

        while (import_list->u1.Ordinal)
        {
            if (IMAGE_SNAP_BY_ORDINAL( import_list->u1.Ordinal ))
            {
                int ordinal = IMAGE_ORDINAL( import_list->u1.Ordinal ) - ntdll_exports->Base;
                thunk_list->u1.Function = find_ordinal_export( ntdll_module, ntdll_exports, ordinal );
                if (!thunk_list->u1.Function) ERR( "%s: ntdll.%u not found\n", debugstr_a(name), ordinal );
            }
            else  /* import by name */
            {
                IMAGE_IMPORT_BY_NAME *pe_name = get_rva( module, import_list->u1.AddressOfData );
                thunk_list->u1.Function = find_pe_export( ntdll_module, ntdll_exports, pe_name );
                if (!thunk_list->u1.Function) ERR( "%s: ntdll.%s not found\n", debugstr_a(name), pe_name->Name );
            }
            import_list++;
            thunk_list++;
        }
    }
    return STATUS_SUCCESS;
}

static void load_ntdll_functions( HMODULE module )
{
    void **ptr;

    ntdll_exports = get_export_dir( module );
    assert( ntdll_exports );

#define GET_FUNC(name) \
    if (!(p##name = (void *)find_named_export( module, ntdll_exports, #name ))) \
        ERR( "%s not found\n", #name )

    GET_FUNC( DbgUiRemoteBreakin );
    GET_FUNC( KiRaiseUserExceptionDispatcher );
    GET_FUNC( KiUserExceptionDispatcher );
    GET_FUNC( KiUserApcDispatcher );
    GET_FUNC( LdrInitializeThunk );
    GET_FUNC( RtlUserThreadStart );
    GET_FUNC( __wine_set_unix_funcs );
#undef GET_FUNC
#define SET_PTR(name,val) \
    if ((ptr = (void *)find_named_export( module, ntdll_exports, #name ))) *ptr = val; \
    else ERR( "%s not found\n", #name )

    SET_PTR( __wine_syscall_dispatcher, __wine_syscall_dispatcher );
#ifdef __i386__
    SET_PTR( __wine_ldt_copy, &__wine_ldt_copy );
#endif
#undef SET_PTR
}

static void check_command_line( int argc, char *argv[] )
{
    static const char usage[] =
        "Usage: wine PROGRAM [ARGUMENTS...]   Run the specified program\n"
        "       wine --help                   Display this help and exit\n"
        "       wine --version                Output version information and exit";

/***********************************************************************
 *           __wine_main
 *
 * Main entry point called by the wine loader.
 */
void __wine_main( int argc, char *argv[], char *envp[] )
{
    init_paths( argv );

    if (!getenv( "WINELOADERNOEXEC" ))  /* first time around */
    {
        check_command_line( argc, argv );
        if (pre_exec())
        {
            static char noexec[] = "WINELOADERNOEXEC=1";
            char **new_argv = malloc( (argc + 2) * sizeof(*argv) );

            memcpy( new_argv + 1, argv, (argc + 1) * sizeof(*argv) );
            putenv( noexec );
            loader_exec( argv0, new_argv, client_cpu );
            fatal_error( "could not exec the wine loader\n" );
        }
    }

#ifdef RLIMIT_NOFILE
    set_max_limit( RLIMIT_NOFILE );
#endif
#ifdef RLIMIT_AS
    set_max_limit( RLIMIT_AS );
#endif

    virtual_init();
    init_environment( argc, argv, envp );

#ifdef __APPLE__
    apple_main_thread();
#endif
    start_main_thread();
}
View Code
复制代码

loader/main.c:

复制代码
extern char **environ;

/* the preloader will set this variable */
const struct wine_preload_info *wine_main_preload_info = NULL;

/* canonicalize path and return its directory name */
static char *realpath_dirname( const char *name )
{
    char *p, *fullpath = realpath( name, NULL );

    if (fullpath)
    {
        p = strrchr( fullpath, '/' );
        if (p == fullpath) p++;
        if (p) *p = 0;
    }
    return fullpath;
}

/* if string ends with tail, remove it */
static char *remove_tail( const char *str, const char *tail )
{
    size_t len = strlen( str );
    size_t tail_len = strlen( tail );
    char *ret;

    if (len < tail_len) return NULL;
    if (strcmp( str + len - tail_len, tail )) return NULL;
    ret = malloc( len - tail_len + 1 );
    memcpy( ret, str, len - tail_len );
    ret[len - tail_len] = 0;
    return ret;
}

/* build a path from the specified dir and name */
static char *build_path( const char *dir, const char *name )
{
    size_t len = strlen( dir );
    char *ret = malloc( len + strlen( name ) + 2 );

    memcpy( ret, dir, len );
    if (len && ret[len - 1] != '/') ret[len++] = '/';
    strcpy( ret + len, name );
    return ret;
}

static const char *get_self_exe( char *argv0 )
{
#if defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__NetBSD__)
    return "/proc/self/exe";
#elif defined (__FreeBSD__) || defined(__DragonFly__)
    return "/proc/curproc/file";
#else
    if (!strchr( argv0, '/' )) /* search in PATH */
    {
        char *p, *path = getenv( "PATH" );

        if (!path || !(path = strdup(path))) return NULL;
        for (p = strtok( path, ":" ); p; p = strtok( NULL, ":" ))
        {
            char *name = build_path( p, argv0 );
            if (!access( name, X_OK ))
            {
                free( path );
                return name;
            }
            free( name );
        }
        free( path );
        return NULL;
    }
    return argv0;
#endif
}

static void *try_dlopen( const char *dir, const char *name )
{
    char *path = build_path( dir, name );
    void *handle = dlopen( path, RTLD_NOW );
    free( path );
    return handle;
}

static void *load_ntdll( char *argv0 )
{
    const char *self = get_self_exe( argv0 );
    char *path, *p;
    void *handle = NULL;

    if (self && ((path = realpath_dirname( self ))))
    {
        if ((p = remove_tail( path, "/loader" )))
        {
            handle = try_dlopen( p, "dlls/ntdll/ntdll.so" );
            free( p );
        }
        else handle = try_dlopen( path, BIN_TO_DLLDIR "/ntdll.so" );
        free( path );
    }

    if (!handle && (path = getenv( "WINEDLLPATH" )))
    {
        path = strdup( path );
        for (p = strtok( path, ":" ); p; p = strtok( NULL, ":" ))
        {
            handle = try_dlopen( p, "ntdll.so" );
            if (handle) break;
        }
        free( path );
    }

    if (!handle && !self) handle = try_dlopen( DLLDIR, "ntdll.so" );

    return handle;
}


/**********************************************************************
 *           main
 */
int main( int argc, char *argv[] )
{
    void *handle;

    if ((handle = load_ntdll( argv[0] )))
    {
        void (*init_func)(int, char **, char **) = dlsym( handle, "__wine_main" );
        if (init_func) init_func( argc, argv, environ );
        fprintf( stderr, "wine: __wine_main function not found in ntdll.so\n" );
        exit(1);
    }

    fprintf( stderr, "wine: could not load ntdll.so: %s\n", dlerror() );
    pthread_detach( pthread_self() );  /* force importing libpthread for OpenGL */
    exit(1);
}
View Code
复制代码

六级/考研单词: implement, compatible, norm, translate, instruct, convert, equivalent, protocol, compile, usage, execute, parcel, dump, utility, thumb, dynamic, issue, compute, ace, seldom, indigenous, outer, infrastructure, audio

posted @   Fun_with_Words  阅读(124)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理









 和5张牌。

点击右上角即可分享
微信分享提示