接上篇Linker源码详解(一),本文继续来分析Linker的链接过程。为了更好的理解Unidbg的原理,我们需要了解很多细节。虽然一个模拟二进制执行框架的弊端很多,但也是未来二进制分析的一个很好的思路。
上篇文章我们讲解了Linker的装载,将So文件按PT_LOAD段的指示来将So加载到内存,那么我们这篇文章就来分析一下加载完之后又干了什么呢?
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#702
static soinfo* load_library(const char* name) {
//...
ElfReader elf_reader(name, fd);
if (!elf_reader.Load()) {
return NULL;
}
const char* bname = strrchr(name, '/');
soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
if (si == NULL) {
return NULL;
}
si->base = elf_reader.load_start();
si->size = elf_reader.load_size();
si->load_bias = elf_reader.load_bias();
si->flags = 0;
si->entry = 0;
si->dynamic = NULL;
si->phnum = elf_reader.phdr_count();
si->phdr = elf_reader.loaded_phdr();
return si;
}
上篇我们进入了elf_reader.Load()函数,阅读了Linker的装载源码,当装载结束后,对soinfo结构体进行赋值(So文件的头信息/装载的结果),并插入到链表,接着我们回到上层函数继续看。 http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#751
static soinfo* find_library_internal(const char* name) {
//...
si = load_library(name);
if (si == NULL) {
return NULL;
}
// At this point we know that whatever is loaded @ base is a valid ELF
// shared library whose segments are properly mapped in.
TRACE("[ init_library base=0x%08x sz=0x%08x name='%s' ]",
si->base, si->size, si->name);
if (!soinfo_link_image(si)) {
munmap(reinterpret_cast<void*>(si->base), si->size);
soinfo_free(si);
return NULL;
}
return si;
}
我们从上面这个函数中看到,当调用了load_library函数之后,又调用了soinfo_link_image这个函数。这个函数也就是我们今天分析的一个主要入口--链接。
下面的这个函数很长,我给大家把不相关的代码去掉,大家先通过注释来看一遍这个函数在干什么。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#1303
static bool soinfo_link_image(soinfo* si) {
//拿到地址、段表指针、段表数
Elf32_Addr base = si->load_bias;
const Elf32_Phdr *phdr = si->phdr;
int phnum = si->phnum;
//...
size_t dynamic_count;
Elf32_Word dynamic_flags;
//这个函数很简单,就是遍历段表,找到类型为PT_DYNAMIC的段
phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic,
&dynamic_count, &dynamic_flags);
if (si->dynamic == NULL) {
if (!relocating_linker) {
DL_ERR("missing PT_DYNAMIC in \"%s\"", si->name);
}
return false;
}
#ifdef ANDROID_ARM_LINKER
//异常相关,有兴趣的同学可以看看
(void) phdr_table_get_arm_exidx(phdr, phnum, base,
&si->ARM_exidx, &si->ARM_exidx_count);
#endif
//上面我们解析到了Dynamic段的地址跟数量,下面就开始遍历Dynamic信息
uint32_t needed_count = 0;
//DT_NULL表示结束
for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
DEBUG("d = %p, d[0](tag) = 0x%08x d[1](val) = 0x%08x", d, d->d_tag, d->d_un.d_val);
switch(d->d_tag){
case DT_HASH:
//哈希表
si->nbucket = ((unsigned *) (base + d->d_un.d_ptr))[0];
si->nchain = ((unsigned *) (base + d->d_un.d_ptr))[1];
si->bucket = (unsigned *) (base + d->d_un.d_ptr + 8);
si->chain = (unsigned *) (base + d->d_un.d_ptr + 8 + si->nbucket * 4);
break;
case DT_STRTAB:
//字符串表
si->strtab = (const char *) (base + d->d_un.d_ptr);
break;
case DT_SYMTAB:
//符号表
si->symtab = (Elf32_Sym *) (base + d->d_un.d_ptr);
break;
case DT_PLTREL:
//未处理
if (d->d_un.d_val != DT_REL) {
DL_ERR("unsupported DT_RELA in \"%s\"", si->name);
return false;
}
break;
case DT_JMPREL:
//PLT重定位表
si->plt_rel = (Elf32_Rel*) (base + d->d_un.d_ptr);
break;
case DT_PLTRELSZ:
//PLT重定位表大小
si->plt_rel_count = d->d_un.d_val / sizeof(Elf32_Rel);
break;
case DT_REL:
//重定位表
si->rel = (Elf32_Rel*) (base + d->d_un.d_ptr);
break;
case DT_RELSZ:
//重定位表大小
si->rel_count = d->d_un.d_val / sizeof(Elf32_Rel);
break;
case DT_PLTGOT:
//GOT全局偏移表,跟PLT延时绑定相关,此处未处理,在Unidbg中也没有处理此项
si->plt_got = (unsigned *)(base + d->d_un.d_ptr);
break;
case DT_DEBUG:
//调试相关, Unidbg未处理,不必理会
if ((dynamic_flags & PF_W) != 0) {
d->d_un.d_val = (int) &_r_debug;
}
break;
case DT_RELA:
//RELA表跟REL表在Unidbg中的处理方案是相同的,这两个值有哪个就用哪个,RELA只是比REL表多了一个adden常量
DL_ERR("unsupported DT_RELA in \"%s\"", si->name);
return false;
case DT_INIT:
//初始化函数
si->init_func = reinterpret_cast<linker_function_t>(base + d->d_un.d_ptr);
DEBUG("%s constructors (DT_INIT) found at %p", si->name, si->init_func);
break;
case DT_FINI:
//析构函数
si->fini_func = reinterpret_cast<linker_function_t>(base + d->d_un.d_ptr);
DEBUG("%s destructors (DT_FINI) found at %p", si->name, si->fini_func);
break;
case DT_INIT_ARRAY:
//init.array 初始化函数列表,后面我们会看到这些初始化函数的调用顺序
si->init_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
DEBUG("%s constructors (DT_INIT_ARRAY) found at %p", si->name, si->init_array);
break;
case DT_INIT_ARRAYSZ:
//init.array 大小
si->init_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr);
break;
case DT_FINI_ARRAY:
//析构函数列表
si->fini_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
DEBUG("%s destructors (DT_FINI_ARRAY) found at %p", si->name, si->fini_array);
break;
case DT_FINI_ARRAYSZ:
//fini.array 大小
si->fini_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr);
break;
case DT_PREINIT_ARRAY:
//也是初始化函数,但是跟init.array不同,这个段大多只出现在可执行文件中,在So中我选择了忽略
si->preinit_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
DEBUG("%s constructors (DT_PREINIT_ARRAY) found at %p", si->name, si->preinit_array);
break;
case DT_PREINIT_ARRAYSZ:
//preinit 列表大小
si->preinit_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr);
break;
case DT_TEXTREL:
si->has_text_relocations = true;
break;
case DT_SYMBOLIC:
si->has_DT_SYMBOLIC = true;
break;
case DT_NEEDED:
//当前So的依赖
++needed_count;
break;
#if defined DT_FLAGS
// TODO: why is DT_FLAGS not defined?
case DT_FLAGS:
if (d->d_un.d_val & DF_TEXTREL) {
si->has_text_relocations = true;
}
if (d->d_un.d_val & DF_SYMBOLIC) {
si->has_DT_SYMBOLIC = true;
}
break;
#endif
}
}
//... Sanity checks.
//至此,Dynamic段的信息就解析完毕了,其中想表达的信息也被处理后放到了soinfo中,后面直接就可以拿来用了
// 开辟依赖库的soinfo空间,准备处理依赖
soinfo** needed = (soinfo**) alloca((1 + needed_count) * sizeof(soinfo*));
soinfo** pneeded = needed;
//再次遍历Dynamic段
for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_NEEDED) {
//查找DT_NEEDED项
const char* library_name = si->strtab + d->d_un.d_val;
DEBUG("%s needs %s", si->name, library_name);
//进行依赖处理,跟加载so一样的路线,还是已加载直接返回,未加载进行查找加载
soinfo* lsi = find_library(library_name);
if (lsi == NULL) {
strlcpy(tmp_err_buf, linker_get_error_buffer(), sizeof(tmp_err_buf));
DL_ERR("could not load library \"%s\" needed by \"%s\"; caused by %s",
library_name, si->name, tmp_err_buf);
return false;
}
*pneeded++ = lsi;
}
}
*pneeded = NULL;
//至此依赖库也已经加载完毕
//处理重定位
if (si->plt_rel != NULL) {
DEBUG("[ relocating %s plt ]", si->name );
if (soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed)) {
return false;
}
}
if (si->rel != NULL) {
DEBUG("[ relocating %s ]", si->name );
if (soinfo_relocate(si, si->rel, si->rel_count, needed)) {
return false;
}
}
//设置soinfo的LINKED标志,表示已进行链接
si->flags |= FLAG_LINKED;
DEBUG("[ finished linking %s ]", si->name);
//...
return true;
}
上面的函数虽然很长,但是它想表达的意思很简单,我们再来回顾下它干了什么事情:
下面我们就来分析它的soinfo_relocate函数,我们看到它调用了两次,只不过入参不同,分别是我们的重定位表和PLT重定位表。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#848
static int soinfo_relocate(soinfo* si, Elf32_Rel* rel, unsigned count,
soinfo* needed[])
{
//拿到符号表和字符串表,定义一些变量
Elf32_Sym* symtab = si->symtab;
const char* strtab = si->strtab;
Elf32_Sym* s;
Elf32_Rel* start = rel;
soinfo* lsi;
//遍历重定位表
for (size_t idx = 0; idx < count; ++idx, ++rel) {
//拿到重定位类型
unsigned type = ELF32_R_TYPE(rel->r_info);
//拿到重定位符号
unsigned sym = ELF32_R_SYM(rel->r_info);
//计算需要重定位的地址
Elf32_Addr reloc = static_cast<Elf32_Addr>(rel->r_offset + si->load_bias);
Elf32_Addr sym_addr = 0;
char* sym_name = NULL;
DEBUG("Processing '%s' relocation at index %d", si->name, idx);
if (type == 0) { // R_*_NONE
continue;
}
if (sym != 0) {
//如果sym不为0,说明重定位需要用到符号,先来找符号,拿到符号名
sym_name = (char *)(strtab + symtab[sym].st_name);
//下面这个函数大家有兴趣的可以看一下,就是根据符号名来从依赖so中查找所需要的符号
s = soinfo_do_lookup(si, sym_name, &lsi, needed);
if (s == NULL) {
//如果没找到,就用本身So的符号
s = &symtab[sym];
if (ELF32_ST_BIND(s->st_info) != STB_WEAK) {
DL_ERR("cannot locate symbol \"%s\" referenced by \"%s\"...", sym_name, si->name);
return -1;
}
switch (type) {
//下面是如果符号不为外部符号,就只能为以下几种类型
#if defined(ANDROID_ARM_LINKER)
case R_ARM_JUMP_SLOT:
case R_ARM_GLOB_DAT:
case R_ARM_ABS32:
case R_ARM_RELATIVE: /* Don't care. */
#endif /* ANDROID_*_LINKER */
/* sym_addr was initialized to be zero above or relocation
code below does not care about value of sym_addr.
No need to do anything. */
break;
#if defined(ANDROID_ARM_LINKER)
case R_ARM_COPY:
/* Fall through. Can't really copy if weak symbol is
not found in run-time. */
#endif /* ANDROID_ARM_LINKER */
default:
DL_ERR("unknown weak reloc type %d @ %p (%d)",
type, rel, (int) (rel - start));
return -1;
}
} else {
//如果我们找到了外部符号,取到外部符号的地址
sym_addr = static_cast<Elf32_Addr>(s->st_value + lsi->load_bias);
}
count_relocation(kRelocSymbol);
} else {
//如果sym为0,就说明当前重定位用不到符号
s = NULL;
}
//下面根据重定位类型来处理重定位
switch(type){
#if defined(ANDROID_ARM_LINKER)
case R_ARM_JUMP_SLOT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO JMP_SLOT %08x <- %08x %s", reloc, sym_addr, sym_name);
//直接将需要重定位的地方,写入获取到的符号地址
*reinterpret_cast<Elf32_Addr*>(reloc) = sym_addr;
break;
case R_ARM_GLOB_DAT:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO GLOB_DAT %08x <- %08x %s", reloc, sym_addr, sym_name);
//直接将需要重定位的地方,写入获取到的符号地址,与R_ARM_JUMP_SLOT相同
*reinterpret_cast<Elf32_Addr*>(reloc) = sym_addr;
break;
case R_ARM_ABS32:
count_relocation(kRelocAbsolute);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO ABS %08x <- %08x %s", reloc, sym_addr, sym_name);
//先读出需要重定位地方的数据,将其和符号地址相加,写入需要重定位的地方
*reinterpret_cast<Elf32_Addr*>(reloc) += sym_addr;
break;
case R_ARM_REL32:
count_relocation(kRelocRelative);
MARK(rel->r_offset);
TRACE_TYPE(RELO, "RELO REL32 %08x <- %08x - %08x %s",
reloc, sym_addr, rel->r_offset, sym_name);
//先读出需要重定位地方的数据,将其和符号地址相加,再与重定位的地址相减,重定位的写入需要重定位的地方。此处Unidbg并未处理,也可忽略,应该是用不到的
*reinterpret_cast<Elf32_Addr*>(reloc) += sym_addr - rel->r_offset;
break;
#endif /* ANDROID_*_LINKER */
#if defined(ANDROID_ARM_LINKER)
case R_ARM_RELATIVE:
#endif /* ANDROID_*_LINKER */
count_relocation(kRelocRelative);
MARK(rel->r_offset);
if (sym) {
DL_ERR("odd RELATIVE form...");
return -1;
}
TRACE_TYPE(RELO, "RELO RELATIVE %08x <- +%08x", reloc, si->base);
//先读出需要重定位地方的数据,将其和So的基址相加,写入需要重定位的地方
*reinterpret_cast<Elf32_Addr*>(reloc) += si->base;
break;
#ifdef ANDROID_ARM_LINKER
case R_ARM_COPY:
//.. 进行了一些错误处理
break;
#endif /* ANDROID_ARM_LINKER */
default:
DL_ERR("unknown reloc type %d @ %p (%d)",
type, rel, (int) (rel - start));
return -1;
}
}
return 0;
}
上面这个函数就是在处理重定位相关的信息了,我们看到从Dynamic段中拿到的跟重定位相关的表,会经过这个函数来处理,将So本身的地址引用进行重定位,使其可以正常运行。其实在32位So中,需要处理的重定位类型并不是很多,就4种类型需要处理,而且还有两种处理方式相同。
现在So就重定位完成了,现在So已经就可以跑起来了,下面我们就来看看从Dynamic段中拿到的各种初始化函数是怎么处理的,还记得吧。 我们回到do_dlopen函数。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#823
soinfo* do_dlopen(const char* name, int flags) {
if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) {
DL_ERR("invalid flags to dlopen: %x", flags);
return NULL;
}
set_soinfo_pool_protection(PROT_READ | PROT_WRITE);
soinfo* si = find_library(name);
if (si != NULL) {
si->CallConstructors();
}
set_soinfo_pool_protection(PROT_READ);
return si;
}
此时我们的find_library函数已经处理完了,So已经被装载且链接过了,最后一步它调用了soinfo的CallConstructors函数,我们来看看这个函数处理了什么。
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#1192
void soinfo::CallConstructors() {
if (constructors_called) {
return;
}
constructors_called = true;
if ((flags & FLAG_EXE) == 0 && preinit_array != NULL) {
// The GNU dynamic linker silently ignores these, but we warn the developer.
PRINT("\"%s\": ignoring %d-entry DT_PREINIT_ARRAY in shared library!",
name, preinit_array_count);
}
//如果Dynamic段不为空,先处理依赖库的初始化
if (dynamic != NULL) {
for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_NEEDED) {
const char* library_name = strtab + d->d_un.d_val;
TRACE("\"%s\": calling constructors in DT_NEEDED \"%s\"", name, library_name);
find_loaded_library(library_name)->CallConstructors();
}
}
}
TRACE("\"%s\": calling constructors", name);
//我们来看下面一句英文注释,非常重要。他说如果DT_INIT和DT_INIT_ARRAY都存在,DT_INIT应该在DT_INIT_ARRAY之前被调用
// DT_INIT should be called before DT_INIT_ARRAY if both are present.
//下面就是在调用两者,CallArray只是在循环调用CallFunction,我们看一下CallFunction
CallFunction("DT_INIT", init_func);
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
}
http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#1172
void soinfo::CallFunction(const char* function_name UNUSED, linker_function_t function) {
if (function == NULL || reinterpret_cast<uintptr_t>(function) == static_cast<uintptr_t>(-1)) {
return;
}
TRACE("[ Calling %s @ %p for '%s' ]", function_name, function, name);
//在这里被调用了,其他没啥好说的
function();
TRACE("[ Done calling %s @ %p for '%s' ]", function_name, function, name);
// The function may have called dlopen(3) or dlclose(3), so we need to ensure our data structures
// are still writable. This happens with our debug malloc (see http://b/7941716).
set_soinfo_pool_protection(PROT_READ | PROT_WRITE);
}
至此,Linker就分析结束了。
我们在最后说一个Unidbg细节的bug,但是现在已经被修复了,就是作为一个扩展吧。我们来看下面一段Unidbg加载So的代码。
if (elfFile.file_type == ElfFile.FT_DYN) { // not executable
int init = dynamicStructure.getInit();
if (init != 0) {
initFunctionList.add(new LinuxInitFunction(load_base, soName, init));
//new LinuxInitFunction(load_base, soName, init).call(emulator);
}
int initArraySize = dynamicStructure.getInitArraySize();
int count = initArraySize / emulator.getPointerSize();
if (count > 0) {
Pointer pointer = UnidbgPointer.pointer(emulator, load_base + dynamicStructure.getInitArrayOffset());
if (pointer == null) {
throw new IllegalStateException("DT_INIT_ARRAY is null");
}
for (int i = 0; i < count; i++) {
Pointer func = pointer.getPointer((long) i * emulator.getPointerSize());
if (func != null) {
initFunctionList.add(new AbsoluteInitFunction(load_base, soName, ((UnidbgPointer) func).peer));
}
}
}
}
如果我们细心的阅读Linker的源码,就会发现Unidbg这里处理的是不恰当的。在本文的最后,我们看到了初始化函数的调用,是DT_INIT函数先被执行,后面再处理DT_INIT_ARRAY,而Unidbg这里就是将他们都添加到一个List,再一起调用。
这样就会产生一个问题,在某些加壳的So中,它的DT_INIT_ARRAY是在DT_INIT函数执行之后,才会有值的(进行修复),所以按照Unidbg这个写法就无法执行INIT_ARRAY或部分INIT_ARRAY无法执行。处理方法也很简单,注释在上面了,只需要让DT_INIT先执行就可以了。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/D5PgJeP9M7obmmHBLvqxAg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。